@@ -15,7 +15,7 @@ The client operates in three modes:
15151 . ** Production (Gateway Mode):** Traffic flows from the Client -> Cloud Load Balancer (Gateway)
1616 -> Router Service -> Sandbox Pod. The client watches the Gateway resource for an external IP.
17172 . ** Development (Port-Forward Mode):** Traffic flows from the Client -> SPDY tunnel -> Router
18- Service -> Sandbox Pod. Uses ` client-go/tools/portforward ` natively — no ` kubectl ` required.
18+ Service -> Sandbox Pod. Uses ` client-go/tools/portforward ` natively, no ` kubectl ` required.
19193 . ** Advanced / Internal Mode:** The client connects directly to a provided ` APIURL ` , bypassing
2020 discovery. Useful for in-cluster agents or custom domains.
2121
@@ -25,7 +25,7 @@ The client operates in three modes:
2525- The [ ** Agent Sandbox Controller** ] ( https://github.com/kubernetes-sigs/agent-sandbox?tab=readme-ov-file#installation ) installed.
2626- The ** Sandbox Router** deployed in the target namespace (` sandbox-router-svc ` ).
2727- A ` SandboxTemplate ` created in the target namespace.
28- - Go 1.26+.
28+ - Go 1.26.1 +.
2929
3030## Installation
3131
@@ -41,7 +41,7 @@ Use this when running against a cluster with a public Gateway IP. The client aut
4141discovers the Gateway address.
4242
4343``` go
44- client , err := sandbox.NewClient ( sandbox.Options {
44+ client , err := sandbox.New (ctx, sandbox.Options {
4545 TemplateName : " my-sandbox-template" ,
4646 GatewayName : " external-http-gateway" ,
4747 GatewayNamespace : " default" ,
@@ -64,7 +64,7 @@ Use this for local development or CI. If you omit `GatewayName` and `APIURL`, th
6464automatically establishes an SPDY port-forward tunnel to the Router Service.
6565
6666``` go
67- client , err := sandbox.NewClient ( sandbox.Options {
67+ client , err := sandbox.New (ctx, sandbox.Options {
6868 TemplateName : " my-sandbox-template" ,
6969 Namespace : " default" ,
7070})
@@ -87,7 +87,7 @@ Use `APIURL` to bypass discovery entirely. Useful for:
8787- ** Custom Domains:** Connecting via HTTPS (e.g., ` https://sandbox.example.com ` ).
8888
8989``` go
90- client , err := sandbox.NewClient ( sandbox.Options {
90+ client , err := sandbox.New (ctx, sandbox.Options {
9191 TemplateName : " my-sandbox-template" ,
9292 APIURL : " http://sandbox-router-svc.default.svc.cluster.local:8080" ,
9393 Namespace : " default" ,
@@ -108,7 +108,7 @@ fmt.Println(entries)
108108If your sandbox runtime listens on a port other than 8888, specify ` ServerPort ` .
109109
110110``` go
111- client , err := sandbox.NewClient ( sandbox.Options {
111+ client , err := sandbox.New (ctx, sandbox.Options {
112112 TemplateName : " my-sandbox-template" ,
113113 ServerPort : 3000 ,
114114})
@@ -117,8 +117,8 @@ client, err := sandbox.NewClient(sandbox.Options{
117117### File Operations
118118
119119``` go
120- // Write a file (only the base filename is sent; directory components are discarded ).
121- // Paths like "", ".", "..", and "/ " are rejected with an error.
120+ // Write a file (must be a plain filename, no directory separators ).
121+ // Paths like "dir/script.py " are rejected with an error.
122122err := client.Write (ctx, " script.py" , []byte (" print('hello')" ))
123123
124124// Read a file
@@ -128,13 +128,15 @@ data, err := client.Read(ctx, "script.py")
128128exists , err := client.Exists (ctx, " script.py" )
129129```
130130
131+ ` Run() ` responses are capped at 16 MB; ` List() ` /` Exists() ` at 8 MB.
132+
131133### 5. Custom TLS / Transport
132134
133135If your Gateway uses HTTPS with a private CA, provide a custom transport:
134136
135137``` go
136138tlsConfig := &tls.Config {RootCAs: myCAPool}
137- client , err := sandbox.NewClient ( sandbox.Options {
139+ client , err := sandbox.New (ctx, sandbox.Options {
138140 TemplateName : " my-sandbox-template" ,
139141 GatewayName : " external-https-gateway" ,
140142 GatewayScheme : " https" ,
@@ -147,37 +149,89 @@ client, err := sandbox.NewClient(sandbox.Options{
147149All options are documented on the ` Options ` struct in
148150[ options.go] ( sandbox/options.go ) . Key fields:
149151
150- - ` TemplateName ` * (required)* — name of the ` SandboxTemplate ` .
151- - ` GatewayName ` — set to enable Gateway mode.
152- - ` APIURL ` — set for Direct URL mode (takes precedence over ` GatewayName ` ).
153- - ` EnableTracing ` / ` TracerProvider ` — OpenTelemetry integration.
152+ - ` TemplateName ` * (required)* : name of the ` SandboxTemplate ` .
153+ - ` GatewayName ` : set to enable Gateway mode.
154+ - ` APIURL ` : set for Direct URL mode (takes precedence over ` GatewayName ` ).
155+ - ` EnableTracing ` / ` TracerProvider ` : OpenTelemetry integration.
154156
155- Any operation accepts ` WithTimeout ` to override the default request timeout:
157+ Any operation accepts ` WithTimeout ` to override the default request timeout,
158+ or ` WithMaxAttempts ` to control retry behavior:
156159
157160``` go
158161result , err := client.Run (ctx, " make build" , sandbox.WithTimeout (10 *time.Minute ))
159162```
160163
161164## Retry Behavior
162165
163- Operations are automatically retried on 5xx responses and connection errors with
164- exponential backoff. See constants in [ transport.go] ( sandbox/transport.go ) for details.
166+ File operations (` Read ` , ` Write ` , ` List ` , ` Exists ` ) are automatically retried (up to
167+ 6 attempts) on 500/502/503/504 responses and connection errors with exponential backoff.
168+
169+ ** Important:** ` Run() ` defaults to a single attempt (no retries) because command
170+ execution is not idempotent. Use ` WithMaxAttempts ` to opt in to retries for
171+ idempotent commands:
172+
173+ ``` go
174+ result , err := client.Run (ctx, " cat /etc/hostname" , sandbox.WithMaxAttempts (6 ))
175+ ```
176+
177+ ## Disconnect / Reconnect
178+
179+ ` Disconnect() ` closes the transport connection ** without deleting** the
180+ SandboxClaim. The sandbox stays alive on the server. Call ` Open() ` to
181+ reconnect to the same sandbox:
182+
183+ ``` go
184+ client.Disconnect (ctx) // transport closed, claim preserved
185+ // ... later ...
186+ client.Open (ctx) // reconnects to the same sandbox
187+ ```
188+
189+ This is useful for suspending a session (e.g., between user requests in a
190+ web service) while keeping the sandbox warm. ` Close() ` deletes the claim;
191+ ` Disconnect(ctx) ` preserves it.
192+
193+ ## Timeouts and Context
194+
195+ ` Open() ` executes several sequential phases (claim creation, sandbox
196+ readiness, transport connection), each bounded by its own timeout
197+ (` SandboxReadyTimeout ` , ` GatewayReadyTimeout ` , ` PortForwardReadyTimeout ` ).
198+ ** Pass a context with a deadline** to bound the total ` Open() ` duration:
199+
200+ ``` go
201+ ctx , cancel := context.WithTimeout (context.Background (), 3 *time.Minute )
202+ defer cancel ()
203+ if err := client.Open (ctx); err != nil { ... }
204+ ```
205+
206+ | Option | Default | Governs |
207+ | --------| ---------| ---------|
208+ | ` SandboxReadyTimeout ` | 180 s | Waiting for the sandbox to become ready |
209+ | ` GatewayReadyTimeout ` | 180 s | Waiting for the gateway IP |
210+ | ` PortForwardReadyTimeout ` | 30 s | Establishing the SPDY tunnel |
211+ | ` CleanupTimeout ` | 30 s | Claim deletion during rollback / Close |
212+ | ` RequestTimeout ` | 180 s | Total timeout per SDK method call (Run, Read, …) |
213+ | ` PerAttemptTimeout ` | 60 s | Time to receive response headers per attempt |
214+ | ` MaxUploadSize ` | 256 MB | Maximum content size for ` Write() ` |
215+ | ` MaxDownloadSize ` | 256 MB | Maximum response body size for ` Read() ` |
165216
166217## Port-Forward Recovery
167218
168219In port-forward mode, a background monitor detects tunnel death and clears the
169220client's ready state. Subsequent operations fail immediately with ` ErrNotReady `
170221(wrapping ` ErrPortForwardDied ` ) instead of timing out.
171222
172- To recover, call ` Open() ` again — the client will verify the claim and sandbox
223+ To recover, call ` Open() ` again. The client will verify the claim and sandbox
173224still exist, then establish a new tunnel:
174225
175226``` go
176227result , err := client.Run (ctx, " echo hi" )
177228if errors.Is (err, sandbox.ErrNotReady ) {
178229 // Port-forward died; reconnect.
179230 if reconnErr := client.Open (ctx); reconnErr != nil {
180- if errors.Is (reconnErr, sandbox.ErrOrphanedClaim ) {
231+ if errors.Is (reconnErr, sandbox.ErrSandboxDeleted ) {
232+ // Claim was deleted externally; start fresh.
233+ reconnErr = client.Open (ctx)
234+ } else if errors.Is (reconnErr, sandbox.ErrOrphanedClaim ) {
181235 // Sandbox no longer ready or verification failed; clean up and start fresh.
182236 client.Close (ctx)
183237 reconnErr = client.Open (ctx)
@@ -194,7 +248,7 @@ If `Close()` fails to delete the claim (e.g., API server unavailable), the clien
194248preserves the claim name so ` Close() ` can be retried to clean up the orphaned claim.
195249Calling ` Open() ` on a client with an orphaned claim returns ` ErrOrphanedClaim ` .
196250
197- ## Error Sentinel Reference
251+ ## Error Reference
198252
199253| Error | Meaning |
200254| -------| ---------|
@@ -208,20 +262,31 @@ Calling `Open()` on a client with an orphaned claim returns `ErrOrphanedClaim`.
208262| ` ErrSandboxDeleted ` | The Sandbox was deleted before becoming ready. |
209263| ` ErrGatewayDeleted ` | The Gateway was deleted during address discovery. |
210264
265+ Non-OK HTTP responses are wrapped in ` *HTTPError ` , which can be extracted
266+ with ` errors.As ` to inspect the status code:
267+
268+ ``` go
269+ var httpErr *sandbox.HTTPError
270+ if errors.As (err, &httpErr) {
271+ fmt.Printf (" status %d : %s \n " , httpErr.StatusCode , httpErr.Body )
272+ }
273+ ```
274+
211275## Testing / Mocking
212276
213277The package exports two interfaces:
214278
215- - ** ` Client ` ** — the core API (` Open ` , ` Close ` , ` Run ` , ` Read ` , ` Write ` , ` List ` ,
216- ` Exists ` , ` IsReady ` ). Accept this in your APIs to enable testing with fakes.
217- - ** ` SandboxInfo ` ** — read-only identity accessors (` ClaimName ` , ` SandboxName ` ,
218- ` PodName ` , ` Annotations ` ). These are on the concrete ` *SandboxClient ` (and the
219- ` SandboxInfo ` interface) rather than ` Client ` , so adding new accessors is not
279+ - ** ` Handle ` ** : the core API (` Open ` , ` Close ` , ` Disconnect(ctx) ` , ` Run ` , ` Read ` , ` Write ` ,
280+ ` List ` , ` Exists ` , ` IsReady ` ). Accept this in your APIs to enable testing with fakes. For
281+ sub-object access (` Commands() ` , ` Files() ` ), use the concrete ` *Sandbox ` type directly.
282+ - ** ` Info ` ** : read-only identity accessors (` ClaimName ` , ` SandboxName ` ,
283+ ` PodName ` , ` Annotations ` ). These are on the concrete ` *Sandbox ` (and the
284+ ` Info ` interface) rather than ` Handle ` , so adding new accessors is not
220285 a breaking change for mock implementors.
221286
222287``` go
223- // Accept the narrow Client interface for testability.
224- func ProcessInSandbox (ctx context .Context , sb sandbox .Client ) error {
288+ // Accept the narrow Handle interface for testability.
289+ func ProcessInSandbox (ctx context .Context , sb sandbox .Handle ) error {
225290 if err := sb.Open (ctx); err != nil {
226291 return err
227292 }
@@ -230,8 +295,8 @@ func ProcessInSandbox(ctx context.Context, sb sandbox.Client) error {
230295 // ...
231296}
232297
233- // When you need identity metadata, accept the concrete type or SandboxInfo .
234- func LogSandboxIdentity (info sandbox .SandboxInfo ) {
298+ // When you need identity metadata, accept the concrete type or Info .
299+ func LogSandboxIdentity (info sandbox .Info ) {
235300 log.Printf (" claim=%s sandbox=%s pod=%s " , info.ClaimName (), info.SandboxName (), info.PodName ())
236301}
237302```
0 commit comments