diff --git a/AGENTS.md b/AGENTS.md index c8a822d6..7ec86a4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,7 +59,8 @@ than regenerating parallel structs. Portable error payloads (`ErrorInfo` / classification are handwritten per language. - Keep portable create and client-config rules in `.proto` (Protovalidate); keep - YAML as a human inventory aligned with those annotations. + YAML as a human inventory aligned with those annotations. Invent handwritten + `ExecOptions` process gates under `ExecOptions` in `specs/validation.yaml`. - Document SDK orchestration that remains handwritten. - Compare references with the pinned SDK version. Resolve conflicting docs explicitly rather than guessing semantics. @@ -73,9 +74,9 @@ classification are handwritten per language. For every public feature, update its language's runnable example and guide. Use small, independent provider-specific projects under -`examples///` in `filesystem/`, `configuration/`, -`lifecycle/`, or `errors/`. Each example demonstrates one operation with its -own configuration and no provider selector or CLI flags. +`examples///` in `filesystem/`, `process/`, +`configuration/`, `lifecycle/`, or `errors/`. Each example demonstrates one +operation with its own configuration and no provider selector or CLI flags. Keep examples aligned with the implemented API. Label test doubles and local verification limits clearly. Verify changed examples before handoff and update diff --git a/README.md b/README.md index 32b480d8..6a86ec6a 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,14 @@ await using client = new Client({ const sandbox = await client.create(); console.log(sandbox.id); + +const result = await sandbox.process.run({ argv: ["echo", "hello from kit"] }); +console.log(result.exitCode, new TextDecoder().decode(result.stdout)); + +await sandbox.filesystem.writeFile( + "/tmp/note.txt", + new TextEncoder().encode("hello\n"), +); ``` Use `await client.close()` in a `finally` block when `await using` is not @@ -90,6 +98,18 @@ func main() { panic(err) } fmt.Println(instance.ID()) + + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, + }) + if err != nil { + panic(err) + } + fmt.Println(result.GetExitCode(), string(result.GetStdout())) + + if err := instance.Filesystem().WriteFile(context.Background(), "/tmp/note.txt", []byte("hello\n"), nil); err != nil { + panic(err) + } } ``` @@ -133,9 +153,10 @@ Modal needs a token pair and an existing app: [TypeScript](examples/typescript/modal/configuration/create-modal-sandbox/README.md) · [Go](examples/go/modal/configuration/create-modal-sandbox/README.md). -Creation examples provision real sandboxes. Filesystem and lifecycle examples +Creation examples provision real sandboxes. Filesystem, process, and lifecycle examples use the ID in `.env` (stop / terminate / archive / delete can destroy that -resource; filesystem examples use the path you set). Error-handling examples +resource; filesystem examples use the path you set; process examples run one +command). Error-handling examples fail locally and create nothing. Skip copying `.env.example` if you already have a configured `.env`. @@ -240,7 +261,7 @@ More detail: [responses and errors](docs/responses-and-errors.md). Guides: [configuration](docs/configuration.md) · [creation](docs/sandbox-creation.md) · [lifecycle](docs/sandbox-lifecycle.md) · -[filesystem](docs/filesystem.md). +[filesystem](docs/filesystem.md), and [process](docs/process.md). ## Documentation @@ -252,20 +273,21 @@ npm install npm run dev ``` -Open http://localhost:3000. Each page covers one part of the client. Code samples switch between TypeScript and Go. GitHub Actions publishes the same site to https://sandbox-kit.github.io/kit/. +Open http://localhost:3000. Docs are organized as Quickstart, Concepts, Providers, Guides, and Examples. Code samples switch between TypeScript and Go. GitHub Actions publishes the same site to https://sandbox-kit.github.io/kit/. These pages are the behavior reference for the same API: -| Guide | Covers | -| -------------------------------------------------------------------------------------------------------------- | --------------------------------- | -| [Client configuration](docs/configuration.md) | Auth, scope, defaults, deadlines | -| [Sandbox creation](docs/sandbox-creation.md) | Fields, units, policies | -| [Sandbox lifecycle](docs/sandbox-lifecycle.md) | Get, attach, capabilities | -| [Filesystem](docs/filesystem.md) | Read, write, copy, list, stat, mkdir, remove | -| [Providers](docs/providers.md) | Daytona and Modal | -| [Responses and errors](docs/responses-and-errors.md) | Error details and provider causes | -| [TS Daytona](sdks/typescript/providers/daytona/README.md) / [Modal](sdks/typescript/providers/modal/README.md) | TypeScript provider notes | -| [Go Daytona](sdks/go/providers/daytona/README.md) / [Modal](sdks/go/providers/modal/README.md) | Go provider notes | +| Guide | Covers | +| -------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| [Client configuration](docs/configuration.md) | Auth, scope, defaults, deadlines | +| [Sandbox creation](docs/sandbox-creation.md) | Fields, units, policies | +| [Sandbox lifecycle](docs/sandbox-lifecycle.md) | Get, attach, capabilities | +| [Filesystem](docs/filesystem.md) | Read, write, copy, list, stat, mkdir, remove | +| [Process](docs/process.md) | Run a command and wait for exit code + output | +| [Providers](docs/providers.md) | Daytona and Modal | +| [Responses and errors](docs/responses-and-errors.md) | Error details and provider causes | +| [TS Daytona](sdks/typescript/providers/daytona/README.md) / [Modal](sdks/typescript/providers/modal/README.md) | TypeScript provider notes | +| [Go Daytona](sdks/go/providers/daytona/README.md) / [Modal](sdks/go/providers/modal/README.md) | Go provider notes | ## License diff --git a/docs/api-declarations.md b/docs/api-declarations.md index eff29d05..36719c95 100644 --- a/docs/api-declarations.md +++ b/docs/api-declarations.md @@ -8,18 +8,18 @@ facades; see [protobuf SDK boundary](protobuf-sdk-boundary.md). YAML does not declare a parallel public API surface. Remaining specs inventory provider rules, validation, and contracts: -| Spec | Role | -| --------------------------------------------- | ----------------------------------------------- | -| `specs/providers/*.yaml` | Provider mapping/check inventories and bindings | -| `specs/validation.yaml` / `specs/client.yaml` | Portable create/config validation inventories | -| `specs/contracts.yaml` | Portable metadata/error contract limits | +| Spec | Role | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `specs/providers/*.yaml` | Provider mapping/check inventories and bindings | +| `specs/validation.yaml` / `specs/client.yaml` | Create/config Protovalidate inventories; `validation.yaml` also lists handwritten `ExecOptions` process gates | +| `specs/contracts.yaml` | Portable metadata/error contract limits | Error payloads (`ErrorInfo` / `ErrorKind`) live in protobuf. Wrappers and provider classification are handwritten per language. ## Handwritten runtime -- `Client` / `Sandbox` facades, lifecycle dispatch, and `Filesystem` +- `Client` / `Sandbox` facades, lifecycle dispatch, `Filesystem`, and `Process` - TypeScript public create/config input types and materialization to wire - Provider selection, native SDK initialization, and cleanup - Error wrappers, cause chaining, and provider-native error classification @@ -27,7 +27,7 @@ provider classification are handwritten per language. - Protovalidate evaluation for shared create/config rules (`secure_endpoint` is handwritten) - Provider checks, field mappings, and config assembly -- Path validation and provider filesystem bindings +- Path validation and provider filesystem / process bindings - Provider orchestration that the mapping inventory cannot express See [Protobuf SDK boundary](protobuf-sdk-boundary.md) and diff --git a/docs/code-generation.md b/docs/code-generation.md index 5d057b98..3e02852a 100644 --- a/docs/code-generation.md +++ b/docs/code-generation.md @@ -57,13 +57,13 @@ the SDK. ## Inputs -| Input | Defines | -| --------------------------------- | ---------------------------------------------------------------------------------- | -| `proto/kit/sandbox/v1/` | Public portable types (config, create, lifecycle options, filesystem options, metadata, errors) | -| `specs/validation.yaml` | Portable create-validation inventory (mirrors Protovalidate options in proto) | -| `specs/client.yaml` | Config validation inventory (mirrors Protovalidate; `secure_endpoint` handwritten) | -| `specs/contracts.yaml` | Metadata/error contract limits | -| `specs/providers/.yaml` | Provider capability, check, mapping, and error-classification inventories | +| Input | Defines | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `proto/kit/sandbox/v1/` | Public portable types (config, create, lifecycle options, filesystem and process options, metadata, errors) | +| `specs/validation.yaml` | Create Protovalidate inventory, plus handwritten `ExecOptions` process gates | +| `specs/client.yaml` | Config validation inventory (mirrors Protovalidate; `secure_endpoint` handwritten) | +| `specs/contracts.yaml` | Metadata/error contract limits | +| `specs/providers/.yaml` | Provider capability, check, mapping, and error-classification inventories | Specs use `version: 1`. Strict loading rejects unknown YAML keys, extra documents, and unsupported versions. Shared fields use protobuf names; SDK symbols belong @@ -141,6 +141,9 @@ Shared create and client-config validation is declared as Protovalidate options on `proto/` messages and evaluated in each SDK (`ValidateCreateOptions` / `ValidateConfig`). Keep `specs/validation.yaml` and `specs/client.yaml` aligned with those annotations. The `secure_endpoint` config check is handwritten. +`ExecOptions` process rules (argv XOR shell, blank shell, absolute working +directory, whole-second provider timeouts) are handwritten in `Process.Run` and +provider adapters; inventory them under `ExecOptions` in `validation.yaml`. Both languages preserve explicit zero/false and supply no defaults. ## Provider checks @@ -251,18 +254,20 @@ Backend state capture and metadata cloning are handwritten in the SDK runtime SDK orchestration that the inventory cannot express stays in handwritten provider integration files (Go `sandbox.go`, `creation.go`, `filesystem.go`, +`process.go`, and, where the provider observes native state, `lifecycle.go` and `metadata.go`; TypeScript provider-named `.ts` plus `mappings.ts` files): -lookup sequences, source selection, readiness, filesystem bindings, and -remaining provider semantics. Handwritten `provider_mappings` / +lookup sequences, source selection, readiness, filesystem and process bindings, +and remaining provider semantics. Handwritten `provider_mappings` / `provider_config` own field conversions and native client configuration; handwritten `provider_checks` enforces create checks; handwritten `errors` classifies native provider errors. -Lifecycle and filesystem option and result types come from protobuf contracts. -Handle methods, typed provider dispatch, lifecycle capability discovery, -filesystem backend methods, and close coordination remain handwritten runtime -code on `Client`, `Sandbox` / `Filesystem`, and provider `Backend` adapters. +Lifecycle, filesystem, and process option and result types come from protobuf +contracts. Handle methods, typed provider dispatch, lifecycle capability +discovery, filesystem and process backend methods, and close coordination +remain handwritten runtime code on `Client`, `Sandbox` / `Filesystem` / +`Process`, and provider `Backend` adapters. See [tooling](../tooling/README.md), [naming](naming.md), and [reference verification](provider-verification.md). diff --git a/docs/filesystem.md b/docs/filesystem.md index fee0ca05..b95f3ebe 100644 --- a/docs/filesystem.md +++ b/docs/filesystem.md @@ -15,8 +15,10 @@ Modal `Attach`, then call `Filesystem` / `sandbox.filesystem`. | Read metadata for one path | `Stat` | `stat` | | Delete a file or directory | `Remove` | `remove` | -Calls on one handle run one at a time, including lifecycle calls. A second -handle for the same sandbox does not wait. +Calls on one handle run one at a time, including lifecycle and process. A +second handle for the same sandbox does not wait. Abort or deadline rejects the +TypeScript `await` (and ends the Go wait), but the handle stays busy until that +provider call settles so the next call does not overlap it. ## Paths diff --git a/docs/naming.md b/docs/naming.md index 3f271d1e..28688488 100644 --- a/docs/naming.md +++ b/docs/naming.md @@ -19,6 +19,7 @@ Use names that read clearly at the call site. Public Go APIs follow Go | Backend result | `CreateResult` | Shared sandbox metadata | | Public handle | `Sandbox` | Identity, copied metadata, lifecycle, and files | | Files in the sandbox | `Filesystem` | Read, write, copy, mkdir, list, stat, and remove | +| Commands in the sandbox | `Process` | Run a command and wait for exit code and output | Public portable types come from protobuf contracts (`kit.sandbox.v1`) aliased in the SDK runtime. TypeScript app-facing `CreateOptions` / `Config` are handwritten @@ -44,13 +45,14 @@ proto/kit/sandbox/v1/ Shared contracts specs/ Portable validation and mapping inventories tooling/ Developer CLI and YAML inventory loaders examples/go// One folder per provider; examples sit under - filesystem/, configuration/, lifecycle/, or errors/ + filesystem/, process/, configuration/, lifecycle/, + or errors/ sdks/typescript/contracts/ Generated protobuf wire types (@sandbox-kit/contracts) sdks/typescript/sandbox/ Public TypeScript SDK (@sandbox-kit/sandbox) src/client/ Client runtime split across focused modules: client.ts (Client + public re-exports), - sandbox.ts (handle), filesystem.ts, types.ts, - errors.ts, validation.ts, operation.ts + sandbox.ts (handle), filesystem.ts, process.ts, + types.ts, errors.ts, validation.ts, operation.ts (deadlines/signals) src/contracts/ Public type aliases over protobuf messages src/metadata/ Metadata map helpers @@ -60,7 +62,8 @@ sdks/typescript/providers/ Optional providers (@sandbox-kit/daytona, @sandbox- src/checks/ Provider create checks src/config/ Native SDK client configuration examples/typescript// One package per example under - filesystem/, configuration/, lifecycle/, or errors/ + filesystem/, process/, configuration/, lifecycle/, + or errors/ ``` Published TypeScript packages expose only their root export (`.`). Cross-folder @@ -79,35 +82,37 @@ Portable messages and enums come from standard protobuf contract packages plus handwritten aliases such as `sandbox_data_contracts.go` / `sandbox_data_contracts.ts`. SDK helpers are handwritten. -| File | Purpose | -| -------------------------------------------------------- | -------------------------------------------------------- | -| `create_validation.go` | Shared CreateOptions validation | -| `sdks/go/internal/sdk` | `RejectUnmapped`, `OptionalDuration`, `WithErrorContext` | -| `field_paths.go` / `config_paths.go` | Canonical field-path constants (Go) | -| `field_paths.ts` / `config_paths.ts` | Same protobuf paths for TS `ErrorInfo.field` | -| `provider_mappings.go` / `mappings/provider_mappings.ts` | Request and metadata field conversions | -| `provider_checks.go` / `checks/provider_checks.ts` | Provider-specific create checks | -| `config/provider_config.ts` | Native SDK configuration assembly (TS) | -| `capabilities.go` | Lifecycle and filesystem backend interfaces (Go) | -| `creation.go` | Provider create planning (Go) | -| `lifecycle.go` | Provider lifecycle calls (Go) | -| `filesystem.go` | Filesystem calls on the sandbox handle (Go) | -| `metadata.go` | Observed sandbox metadata (Go) | -| `operation.go` | Sandbox handle operation gate (Go) | +| File | Purpose | +| -------------------------------------------------------- | ---------------------------------------------------------- | +| `create_validation.go` | Shared CreateOptions validation | +| `sdks/go/internal/sdk` | `RejectUnmapped`, `OptionalDuration`, `WithErrorContext` | +| `field_paths.go` / `config_paths.go` | Canonical field-path constants (Go) | +| `field_paths.ts` / `config_paths.ts` | Same protobuf paths for TS `ErrorInfo.field` | +| `provider_mappings.go` / `mappings/provider_mappings.ts` | Request and metadata field conversions | +| `provider_checks.go` / `checks/provider_checks.ts` | Provider-specific create checks | +| `config/provider_config.ts` | Native SDK configuration assembly (TS) | +| `capabilities.go` | Lifecycle, filesystem, and process backend interfaces (Go) | +| `creation.go` | Provider create planning (Go) | +| `lifecycle.go` | Provider lifecycle calls (Go) | +| `filesystem.go` | Filesystem calls on the sandbox handle (Go) | +| `process.go` | Process.Run on the sandbox handle (Go) | +| `metadata.go` | Observed sandbox metadata (Go) | +| `operation.go` | Sandbox handle operation gate (Go) | Handwritten runtime and provider files own client/orchestration behavior and metadata clone helpers. Go providers keep SDK calls in `sandbox.go` and the create plan in `creation.go`. Daytona also keeps lifecycle calls in `lifecycle.go` and observed metadata in `metadata.go`. Filesystem adapters live -in each provider's `filesystem.go`. Keep YAML inventories aligned when changing -those files. +in each provider's `filesystem.go`. Process adapters live in each provider's +`process.go`. Keep YAML inventories aligned when changing those files. ## Language conventions Field-path constants use names such as `CreateFieldResourcesCpuCores`, `ConfigFieldScopeAppName`, `InfoFieldResources`, `ResultFieldExitCode`, -`FileFieldPath`, and `FileFieldLocalPath`. Their values retain canonical -protobuf field names, including nested paths. +`FileFieldPath`, `FileFieldLocalPath`, `ExecFieldArgv`, and +`ExecFieldWorkingDirectory`. Their values retain canonical protobuf field +names, including nested paths. Protobuf and YAML use snake_case semantic field names. Generated Go contract types follow protobuf Go naming (`CpuCores`, `MemoryMib`, `OutboundCidrs`, diff --git a/docs/process.md b/docs/process.md new file mode 100644 index 00000000..f8fe2571 --- /dev/null +++ b/docs/process.md @@ -0,0 +1,134 @@ +# Process + +Run a command inside an open sandbox and wait for it to finish. Use the same +options on Daytona and Modal. + +| You want to | Go | TypeScript | +| ------------------------------------- | ----- | ---------- | +| Run a command and wait for the result | `Run` | `run` | + +Open a handle with `Create`, Daytona `Get`, or Modal `Attach`, then call +`Process()` / `sandbox.process`. + +Calls on one handle run one at a time (including filesystem and lifecycle). A +second handle for the same sandbox does not wait on the first. + +## Command shape + +Set exactly one of `argv` or `shell`. + +- `argv` — program and arguments, for example `["echo", "hello"]` +- `shell` — one shell command string, for example `"echo hello"` + +Setting both or neither fails with field `argv`. A blank or whitespace-only +`shell` (after trim) fails with field `shell`. + +How providers run the command: + +| You pass | Daytona | Modal | +| -------- | ------------------------- | ------------------------ | +| `argv` | POSIX-quoted shell string | argument list as-is | +| `shell` | shell string as-is | `bash -c` with that text | + +## Working directory and environment + +`workingDirectory` / `WorkingDirectory`, when set, must be an absolute path +such as `/workspace`. Relative, blank, or NUL-containing paths fail with field +`working_directory`. + +`environment` / `Environment` adds variables for that call only: + +- Omit the map to leave the provider default alone. +- Pass an empty map to add no variables for this call. + +## Result + +When the command finishes you get: + +- `exitCode` / `exit_code` +- `stdout` +- `stderr` + +A non-zero exit code is **not** an SDK error. Check the exit code in your +code. + +| Provider | Output | +| -------- | ----------------------------------------------------------------- | +| Daytona | stdout only; `stderr` is empty (Go returns a non-nil empty slice) | +| Modal | both stdout and stderr | + +## Timeouts and cancel + +| Setting | Meaning | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Omit `timeout` | Use the client default deadline for this call | +| `timeout: 0` / `Duration(0)` | Do not add an extra client deadline for this call | +| Positive timeout | Cap this call; must be a whole number of seconds (`5000` ms or `5 * time.Second` is fine; `1500` ms is rejected, field `timeout`) | + +Omit or zero also means **no separate provider command limit**. Provider +quirks: + +- **Daytona** — if you omit a native timeout, Daytona defaults to 10 seconds. + This client therefore uses timeout `0` for omit/zero so the command is not + capped at 10 seconds. +- **Modal** — omit/zero adds no Modal command limit. In TypeScript the client + omits `timeoutMs` (sending `0` is rejected by Modal). + +Cancel behavior: + +- **Go** — cancel or deadline on the context ends the wait and returns an + error. The handle stays busy until that provider call returns. +- **TypeScript** — abort rejects the `await`. The remote command may keep + running; Daytona and Modal exec do not take an `AbortSignal`. The handle + still stays busy until that provider call settles, so the next call on the + same handle does not overlap it. + +```ts +const result = await sandbox.process.run({ + argv: ["echo", "hello"], + timeout: 5_000, +}); +``` + +```go +result, err := instance.Process().Run(ctx, &sandbox.ExecOptions{ + Argv: []string{"echo", "hello"}, + Timeout: sandbox.Duration(5 * time.Second), +}) +``` + +## Run a command + +```ts +const result = await sandbox.process.run({ argv: ["echo", "hello from kit"] }); +console.log(result.exitCode, new TextDecoder().decode(result.stdout)); +``` + +```go +result, err := instance.Process().Run(ctx, &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, +}) +``` + +[Go Daytona](../examples/go/daytona/process/run-daytona-command/README.md) · +[TypeScript Daytona](../examples/typescript/daytona/process/run-daytona-command/README.md) · +[Go Modal](../examples/go/modal/process/run-modal-command/README.md) · +[TypeScript Modal](../examples/typescript/modal/process/run-modal-command/README.md). + +## More examples + +| Topic | Go Daytona | TypeScript Daytona | Go Modal | TypeScript Modal | +| ------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Shell string | [run-daytona-shell-command](../examples/go/daytona/process/run-daytona-shell-command/README.md) | [run-daytona-shell-command](../examples/typescript/daytona/process/run-daytona-shell-command/README.md) | [run-modal-shell-command](../examples/go/modal/process/run-modal-shell-command/README.md) | [run-modal-shell-command](../examples/typescript/modal/process/run-modal-shell-command/README.md) | +| Working directory | [run-daytona-with-cwd](../examples/go/daytona/process/run-daytona-with-cwd/README.md) | [run-daytona-with-cwd](../examples/typescript/daytona/process/run-daytona-with-cwd/README.md) | [run-modal-with-cwd](../examples/go/modal/process/run-modal-with-cwd/README.md) | [run-modal-with-cwd](../examples/typescript/modal/process/run-modal-with-cwd/README.md) | +| Environment | [run-daytona-with-env](../examples/go/daytona/process/run-daytona-with-env/README.md) | [run-daytona-with-env](../examples/typescript/daytona/process/run-daytona-with-env/README.md) | [run-modal-with-env](../examples/go/modal/process/run-modal-with-env/README.md) | [run-modal-with-env](../examples/typescript/modal/process/run-modal-with-env/README.md) | +| Timeout | [run-daytona-with-timeout](../examples/go/daytona/process/run-daytona-with-timeout/README.md) | [run-daytona-with-timeout](../examples/typescript/daytona/process/run-daytona-with-timeout/README.md) | [run-modal-with-timeout](../examples/go/modal/process/run-modal-with-timeout/README.md) | [run-modal-with-timeout](../examples/typescript/modal/process/run-modal-with-timeout/README.md) | +| Non-zero exit code | [run-daytona-check-exit](../examples/go/daytona/process/run-daytona-check-exit/README.md) | [run-daytona-check-exit](../examples/typescript/daytona/process/run-daytona-check-exit/README.md) | [run-modal-check-exit](../examples/go/modal/process/run-modal-check-exit/README.md) | [run-modal-check-exit](../examples/typescript/modal/process/run-modal-check-exit/README.md) | + +## Limits + +Streaming stdin/stdout, long-lived sessions, PTY, and Daytona `code_run` are +not part of this API. + +Closing the handle leaves the sandbox running. Closing the client closes your +connection to the provider. diff --git a/docs/protobuf-sdk-boundary.md b/docs/protobuf-sdk-boundary.md index 6893de91..8ec4d98b 100644 --- a/docs/protobuf-sdk-boundary.md +++ b/docs/protobuf-sdk-boundary.md @@ -10,11 +10,11 @@ types, constructors, methods, errors, or control flow. `sandbox.proto`: - configuration and creation options; -- sandbox metadata, lifecycle options, filesystem options and results, and +- sandbox metadata, lifecycle options, filesystem and process options and results, and portable error details. Kit does not declare a protobuf RPC service. Language SDKs expose handwritten -`Client` / `Sandbox` / `Filesystem` APIs and provider `Backend` adapters over +`Client` / `Sandbox` / `Filesystem` / `Process` APIs and provider `Backend` adapters over vendor SDKs. Generated Go wire output lives under `sdks/go/internal/contracts/` so only @@ -32,7 +32,7 @@ representation of portable data. Handwritten language runtime code owns behavior: -- public `Client`, `Sandbox`, and `Filesystem` facades; +- public `Client`, `Sandbox`, `Filesystem`, and `Process` facades; - TypeScript public create/config input types (materialized to wire privately); - provider selection, native SDK initialization, cleanup, and deadlines; - private adapter interfaces and private native SDK state; diff --git a/docs/provider-verification.md b/docs/provider-verification.md index be39b6c0..c7deb3a0 100644 --- a/docs/provider-verification.md +++ b/docs/provider-verification.md @@ -14,9 +14,11 @@ resolution, and error translation without provisioning cloud resources. Filesystem tests cover path validation, local copy classification, timeouts, and provider file APIs (Modal `readBytes` / `writeBytes` / `copyFromLocal` / `listFiles`; Daytona high-level `FileSystem` plus toolbox list/stat, and -`downloadFiles` with timeout `0` when Kit adds no transfer limit). Provider -tests use the pinned SDK's transport/service seams; they do not expose those -native clients through the public SDK. +`downloadFiles` with timeout `0` when Kit adds no transfer limit). Process +tests cover argv/shell exclusivity, working-directory checks, whole-second +command timeouts, and command mapping (Daytona POSIX-quoted argv; Modal +`bash -c` for shell). Provider tests use the pinned SDK's transport/service +seams; they do not expose those native clients through the public SDK. ## Reference sources diff --git a/docs/providers.md b/docs/providers.md index 2e138e2c..f648d5d7 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -35,6 +35,7 @@ await using client = new Client({ | Start, stop, pause, archive, delete | Unsupported | Supported | | Terminate | Supported | Unsupported | | Filesystem | Same eight shared methods | Same eight shared methods | +| Process | Same blocking `Run` | Same blocking `Run` | `supports` reports whether the provider implements that operation. A missing operation fails before the provider call. Daytona still accepts or rejects a @@ -48,9 +49,11 @@ Closing the client closes your connection to the provider. The sandboxes keep running. Closing a handle leaves the sandbox as it is. You can keep using the handle. The next call returns the provider's result. -The public API covers create, lifecycle, and files inside the sandbox. GPU -requests, resizing, storage volumes, browsers, process execution, and macOS -or Windows creation are outside it. +The public API covers create, lifecycle, files, and blocking command execution +inside the sandbox. GPU requests, resizing, storage volumes, browsers, +streaming process handles, sessions, PTY, and macOS or Windows creation are +outside it. See [configuration](configuration.md), [creation](sandbox-creation.md), -[lifecycle](sandbox-lifecycle.md), and [filesystem](filesystem.md). +[lifecycle](sandbox-lifecycle.md), [filesystem](filesystem.md), and +[process](process.md). diff --git a/docs/sandbox-creation.md b/docs/sandbox-creation.md index 5bc1084a..916366fe 100644 --- a/docs/sandbox-creation.md +++ b/docs/sandbox-creation.md @@ -121,8 +121,9 @@ ready waiting also requires a readiness probe and a positive creation timeout. `Info()` returns a copy of the metadata. Editing that copy leaves the handle unchanged. Errors include a kind, field, and message. The provider's own error stays available as the cause. Go forwards cancellation into the provider call. -TypeScript checks the abort signal before each provider call. Kit does not -retry, and it does not delete a sandbox when a call fails. +TypeScript rejects the `await` on abort; create itself is not queued behind +other client calls. Kit does not retry, and it does not delete a sandbox when a +call fails. Closing the client closes your connection to the provider. The sandbox keeps running. Closing a handle leaves the sandbox as it is. You can keep using the @@ -131,7 +132,9 @@ Lifecycle operations are described in the [lifecycle guide](sandbox-lifecycle.md Files inside a sandbox are described in the [filesystem guide](filesystem.md). Creation covers the sandbox itself. GPU requests, resizing, storage volumes, -browsers, process execution, and macOS or Windows creation are outside it. +browsers, streaming process handles, sessions, PTY, and macOS or Windows +creation are outside it. Blocking command execution is covered in +[process](process.md). See [configuration](configuration.md), [lifecycle](sandbox-lifecycle.md), and [filesystem](filesystem.md). diff --git a/docs/sandbox-lifecycle.md b/docs/sandbox-lifecycle.md index 5b9a0571..9df3e1ba 100644 --- a/docs/sandbox-lifecycle.md +++ b/docs/sandbox-lifecycle.md @@ -74,12 +74,15 @@ the provider's exact value. `Terminal` is present when the provider reports that the sandbox is finished. A later lifecycle call still goes to the provider, and Kit returns that result. -Each SDK serializes lifecycle transitions on one handle. This does not -serialize a different handle or process referring to the same remote sandbox. A -caller waiting behind an in-progress transition can still cancel its context or -abort signal, or reach its deadline. Go forwards that context into the native -SDK. TypeScript checks the abort signal before each native call, so a canceled -operation does not start the next provider request. +Each SDK serializes lifecycle transitions on one handle (with filesystem and +process). This does not serialize a different handle or process referring to +the same remote sandbox. A caller waiting behind an in-progress transition can +still cancel its context or abort signal, or reach its deadline. Go forwards +that context into the native SDK and keeps the handle busy until the provider +call returns. TypeScript rejects the `await` on abort or deadline; Daytona and +Modal often ignore `AbortSignal` mid-call, so the remote work may continue, but +the handle stays busy until that provider call settles. A queued call that is +canceled before it starts never reaches the provider. ## Provider support diff --git a/examples/README.md b/examples/README.md index 37dcd729..46f9a9f7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,9 +2,9 @@ Independent Go and TypeScript projects. Each selects one provider, loads its own `.env`, and demonstrates one operation. Projects for a provider sit in -`filesystem`, `configuration`, `lifecycle`, or `errors`. Every project README -explains what the example does for that provider, with edge cases drawn from -Daytona and Modal behavior. +`filesystem`, `process`, `configuration`, `lifecycle`, or `errors`. Every +project README explains what the example does for that provider, with edge +cases drawn from Daytona and Modal behavior. ## Run one @@ -24,9 +24,10 @@ Modal needs a token pair and an existing app: [Go](go/modal/configuration/create-modal-sandbox/README.md) · [TypeScript](typescript/modal/configuration/create-modal-sandbox/README.md). -Creation examples provision real resources. Filesystem and lifecycle examples -use the sandbox ID in `.env`. A filesystem example runs one file operation -(read, write, copy, mkdir, list, stat, or remove) on the path you set. Stop, +Creation examples provision real resources. Filesystem, process, and lifecycle +examples use the sandbox ID in `.env`. A filesystem example runs one file +operation (read, write, copy, mkdir, list, stat, or remove) on the path you +set. A process example runs one command. Stop, terminate, archive, and delete can destroy the sandbox. Error-handling examples fail locally. `Client.Close` closes your connection to Daytona or Modal. `Sandbox.Close` does not change the sandbox, and you can keep @@ -39,6 +40,12 @@ Indexes: [Go](go/README.md) · [TypeScript](typescript/README.md). | Example | Go | TypeScript | | ------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Daytona creation | [go](go/daytona/configuration/create-daytona-sandbox/README.md) | [typescript](typescript/daytona/configuration/create-daytona-sandbox/README.md) | +| Daytona run command | [go](go/daytona/process/run-daytona-command/README.md) | [typescript](typescript/daytona/process/run-daytona-command/README.md) | +| Daytona shell command | [go](go/daytona/process/run-daytona-shell-command/README.md) | [typescript](typescript/daytona/process/run-daytona-shell-command/README.md) | +| Daytona run with cwd | [go](go/daytona/process/run-daytona-with-cwd/README.md) | [typescript](typescript/daytona/process/run-daytona-with-cwd/README.md) | +| Daytona run with env | [go](go/daytona/process/run-daytona-with-env/README.md) | [typescript](typescript/daytona/process/run-daytona-with-env/README.md) | +| Daytona run with timeout | [go](go/daytona/process/run-daytona-with-timeout/README.md) | [typescript](typescript/daytona/process/run-daytona-with-timeout/README.md) | +| Daytona check exit | [go](go/daytona/process/run-daytona-check-exit/README.md) | [typescript](typescript/daytona/process/run-daytona-check-exit/README.md) | | Daytona read file | [go](go/daytona/filesystem/read-daytona-file/README.md) | [typescript](typescript/daytona/filesystem/read-daytona-file/README.md) | | Daytona write file | [go](go/daytona/filesystem/write-daytona-file/README.md) | [typescript](typescript/daytona/filesystem/write-daytona-file/README.md) | | Daytona copy in | [go](go/daytona/filesystem/copy-file-into-daytona/README.md) | [typescript](typescript/daytona/filesystem/copy-file-into-daytona/README.md) | @@ -71,6 +78,12 @@ Indexes: [Go](go/README.md) · [TypeScript](typescript/README.md). | Daytona close handle | [go](go/daytona/lifecycle/close-daytona-handle/README.md) | [typescript](typescript/daytona/lifecycle/close-daytona-handle/README.md) | | Daytona errors | [go](go/daytona/errors/handle-daytona-errors/README.md) | [typescript](typescript/daytona/errors/handle-daytona-errors/README.md) | | Modal creation | [go](go/modal/configuration/create-modal-sandbox/README.md) | [typescript](typescript/modal/configuration/create-modal-sandbox/README.md) | +| Modal run command | [go](go/modal/process/run-modal-command/README.md) | [typescript](typescript/modal/process/run-modal-command/README.md) | +| Modal shell command | [go](go/modal/process/run-modal-shell-command/README.md) | [typescript](typescript/modal/process/run-modal-shell-command/README.md) | +| Modal run with cwd | [go](go/modal/process/run-modal-with-cwd/README.md) | [typescript](typescript/modal/process/run-modal-with-cwd/README.md) | +| Modal run with env | [go](go/modal/process/run-modal-with-env/README.md) | [typescript](typescript/modal/process/run-modal-with-env/README.md) | +| Modal run with timeout | [go](go/modal/process/run-modal-with-timeout/README.md) | [typescript](typescript/modal/process/run-modal-with-timeout/README.md) | +| Modal check exit | [go](go/modal/process/run-modal-check-exit/README.md) | [typescript](typescript/modal/process/run-modal-check-exit/README.md) | | Modal read file | [go](go/modal/filesystem/read-modal-file/README.md) | [typescript](typescript/modal/filesystem/read-modal-file/README.md) | | Modal write file | [go](go/modal/filesystem/write-modal-file/README.md) | [typescript](typescript/modal/filesystem/write-modal-file/README.md) | | Modal copy in | [go](go/modal/filesystem/copy-file-into-modal/README.md) | [typescript](typescript/modal/filesystem/copy-file-into-modal/README.md) | diff --git a/examples/go/README.md b/examples/go/README.md index 8ce3a0b8..81a92c9d 100644 --- a/examples/go/README.md +++ b/examples/go/README.md @@ -1,8 +1,8 @@ # Go examples Each project uses the sandbox client and one provider from this repository. -Projects sit in `filesystem`, `configuration`, `lifecycle`, or `errors`. -Requires Go 1.26.0 or newer and this checkout. +Projects sit in `filesystem`, `process`, `configuration`, `lifecycle`, or +`errors`. Requires Go 1.26.0 or newer and this checkout. ## Run one @@ -18,8 +18,9 @@ shell variables win. The SDK does not load files. Modal app setup: [create-modal-sandbox](modal/configuration/create-modal-sandbox/README.md). Creation programs provision real sandboxes (linked Daytona creates a parent and -child). Filesystem and lifecycle programs use the configured sandbox ID. Read -the guide before a destructive stop, terminate, archive, delete, or remove. +child). Filesystem, process, and lifecycle programs use the configured sandbox +ID. Read the guide before a destructive stop, terminate, archive, delete, or +remove. Error-handling programs fail locally. Closing a handle leaves the sandbox as it is. Closing the client closes your connection to Daytona or Modal. @@ -30,6 +31,12 @@ closes your connection to Daytona or Modal. | Project | Demonstrates | | -------------------------------------------------------------------------------------------- | --------------------------------------- | | [create-daytona-sandbox](daytona/configuration/create-daytona-sandbox/README.md) | default Daytona snapshot | +| [run-daytona-command](daytona/process/run-daytona-command/README.md) | run one command | +| [run-daytona-shell-command](daytona/process/run-daytona-shell-command/README.md) | run one shell string | +| [run-daytona-with-cwd](daytona/process/run-daytona-with-cwd/README.md) | run with working directory | +| [run-daytona-with-env](daytona/process/run-daytona-with-env/README.md) | run with environment variables | +| [run-daytona-with-timeout](daytona/process/run-daytona-with-timeout/README.md) | run with a whole-second timeout | +| [run-daytona-check-exit](daytona/process/run-daytona-check-exit/README.md) | non-zero exit in the result | | [read-daytona-file](daytona/filesystem/read-daytona-file/README.md) | read one file | | [write-daytona-file](daytona/filesystem/write-daytona-file/README.md) | write one file | | [copy-file-into-daytona](daytona/filesystem/copy-file-into-daytona/README.md) | copy a local file in | @@ -67,6 +74,12 @@ closes your connection to Daytona or Modal. | Project | Demonstrates | | ------------------------------------------------------------------------------------------ | ------------------------------------------ | | [create-modal-sandbox](modal/configuration/create-modal-sandbox/README.md) | image creation with resources and policies | +| [run-modal-command](modal/process/run-modal-command/README.md) | run one command | +| [run-modal-shell-command](modal/process/run-modal-shell-command/README.md) | run one shell string | +| [run-modal-with-cwd](modal/process/run-modal-with-cwd/README.md) | run with working directory | +| [run-modal-with-env](modal/process/run-modal-with-env/README.md) | run with environment variables | +| [run-modal-with-timeout](modal/process/run-modal-with-timeout/README.md) | run with a whole-second timeout | +| [run-modal-check-exit](modal/process/run-modal-check-exit/README.md) | non-zero exit in the result | | [read-modal-file](modal/filesystem/read-modal-file/README.md) | read one file | | [write-modal-file](modal/filesystem/write-modal-file/README.md) | write one file | | [copy-file-into-modal](modal/filesystem/copy-file-into-modal/README.md) | copy a local file in | diff --git a/examples/go/daytona/process/run-daytona-check-exit/.env.example b/examples/go/daytona/process/run-daytona-check-exit/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/go/daytona/process/run-daytona-check-exit/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/go/daytona/process/run-daytona-check-exit/README.md b/examples/go/daytona/process/run-daytona-check-exit/README.md new file mode 100644 index 00000000..7886833c --- /dev/null +++ b/examples/go/daytona/process/run-daytona-check-exit/README.md @@ -0,0 +1,51 @@ +# Check a non-zero Daytona exit code + +Run `exit 7` via `shell` and read `exit_code` from the result. The program +loads that sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- A non-zero exit code is not an SDK error. Check `exit_code` / `exitCode` in + your program. +- Transport, validation, and Daytona API failures still return errors. + +## Run + +```sh +cd examples/go/daytona/process/run-daytona-check-exit +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/daytona/process/run-daytona-check-exit/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/daytona/process/run-daytona-check-exit/go.mod b/examples/go/daytona/process/run-daytona-check-exit/go.mod new file mode 100644 index 00000000..334ec18d --- /dev/null +++ b/examples/go/daytona/process/run-daytona-check-exit/go.mod @@ -0,0 +1,71 @@ +module github.com/sandbox-kit/kit/examples/go/daytona/process/run-daytona-check-exit + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/daytona v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 // indirect + github.com/aws/smithy-go v1.28.5 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 // indirect + github.com/daytona/clients/api-client-go v0.225.0 // indirect + github.com/daytona/clients/sdk-go v0.225.0 // indirect + github.com/daytona/clients/toolbox-api-client-go v0.225.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/otel/sdk v1.47.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.47.0 // indirect + go.opentelemetry.io/otel/trace v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/daytona => ../../../../../sdks/go/providers/daytona + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/daytona/process/run-daytona-check-exit/go.sum b/examples/go/daytona/process/run-daytona-check-exit/go.sum new file mode 100644 index 00000000..d0a9e0cd --- /dev/null +++ b/examples/go/daytona/process/run-daytona-check-exit/go.sum @@ -0,0 +1,129 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= +github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ= +github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0= +github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 h1:q9YuNkYR/7lxflWCcEv3C0cecUt3Zvrh2hn9ZNxieNA= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16/go.mod h1:3BSeJQF55QQI+dDFHx4WTIjTvX/Lvtk8Jaaf/SGAnBM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM= +github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= +github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 h1:vlfG6CqZ0wLdks8dzvNtuo8mBADO/RL0q5J2tseEI4s= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5/go.mod h1:+kRgiUXoVO3BX/VzeNadSCTXPXElAkPk1pNk1haKIDw= +github.com/daytona/clients/api-client-go v0.225.0 h1:C4Ucqync8uhSYkn6uVM7GJ2/O1rGWtJY9F916RRxGXg= +github.com/daytona/clients/api-client-go v0.225.0/go.mod h1:US+IQTDTZam9c4UpPMB3WDYYMIyWSKlgfDZzwWffY94= +github.com/daytona/clients/sdk-go v0.225.0 h1:C0zGXGtwYczb1b/MrX/Tc8YLQJuLQaztnu+u7Tz7i74= +github.com/daytona/clients/sdk-go v0.225.0/go.mod h1:TodsNlnrGObpupcLuxGl2F+kp7yyoFUTBt/CFaLJyqQ= +github.com/daytona/clients/toolbox-api-client-go v0.225.0 h1:tNrV4+lo970ZrNXdOVCR3yxpvRSh9LZL8OtzFupOkDs= +github.com/daytona/clients/toolbox-api-client-go v0.225.0/go.mod h1:lwZA3j823fNNiAgTSMThjZkYWegIigoFIREAtdo7o3E= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM= +go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/daytona/process/run-daytona-check-exit/main.go b/examples/go/daytona/process/run-daytona-check-exit/main.go new file mode 100644 index 00000000..d4c5b85e --- /dev/null +++ b/examples/go/daytona/process/run-daytona-check-exit/main.go @@ -0,0 +1,55 @@ +// Check a non-zero exit code from a Daytona command. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/daytona" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + config := &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.APIKey{ + ApiKey: &sandbox.APIKeyCredentials{ + Key: os.Getenv("DAYTONA_API_KEY"), + }, + }, + }, + Timeout: sandbox.Duration(2 * time.Minute), + } + if endpoint := os.Getenv("DAYTONA_API_URL"); endpoint != "" { + config.Endpoint = sandbox.Value(endpoint) + } + if target := os.Getenv("DAYTONA_TARGET"); target != "" { + config.Region = sandbox.Value(target) + } + client, err := sandbox.NewClient(daytona.New(), config) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Get(context.Background(), os.Getenv("DAYTONA_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Shell: sandbox.Value("exit 7"), + }) + if err != nil { + log.Fatal(err) + } + if result.GetExitCode() != 7 { + log.Fatalf("want exit 7, got %d", result.GetExitCode()) + } + fmt.Printf("command finished with exit=%d\n", result.GetExitCode()) +} diff --git a/examples/go/daytona/process/run-daytona-command/.env.example b/examples/go/daytona/process/run-daytona-command/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/go/daytona/process/run-daytona-command/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/go/daytona/process/run-daytona-command/README.md b/examples/go/daytona/process/run-daytona-command/README.md new file mode 100644 index 00000000..8ae62ff4 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-command/README.md @@ -0,0 +1,54 @@ +# Run a Daytona command + +Run `echo hello from kit` with `Process.Run` / `sandbox.process.run` using +`argv`. The program loads that sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- Set exactly one of `argv` or `shell`. Setting both, or neither, fails before + Daytona is called. The error field is `argv`. +- A relative working directory is rejected before Daytona is called. The error + field is `working_directory`. +- Omit or zero `timeout` so Daytona does not apply its 10-second native + default. Command timeouts must be whole seconds. Cancel the context to stop waiting. + +## Run + +```sh +cd examples/go/daytona/process/run-daytona-command +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/daytona/process/run-daytona-command/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/daytona/process/run-daytona-command/go.mod b/examples/go/daytona/process/run-daytona-command/go.mod new file mode 100644 index 00000000..facf6e3e --- /dev/null +++ b/examples/go/daytona/process/run-daytona-command/go.mod @@ -0,0 +1,71 @@ +module github.com/sandbox-kit/kit/examples/go/daytona/process/run-daytona-command + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/daytona v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 // indirect + github.com/aws/smithy-go v1.28.5 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 // indirect + github.com/daytona/clients/api-client-go v0.225.0 // indirect + github.com/daytona/clients/sdk-go v0.225.0 // indirect + github.com/daytona/clients/toolbox-api-client-go v0.225.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/otel/sdk v1.47.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.47.0 // indirect + go.opentelemetry.io/otel/trace v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/daytona => ../../../../../sdks/go/providers/daytona + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/daytona/process/run-daytona-command/go.sum b/examples/go/daytona/process/run-daytona-command/go.sum new file mode 100644 index 00000000..d0a9e0cd --- /dev/null +++ b/examples/go/daytona/process/run-daytona-command/go.sum @@ -0,0 +1,129 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= +github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ= +github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0= +github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 h1:q9YuNkYR/7lxflWCcEv3C0cecUt3Zvrh2hn9ZNxieNA= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16/go.mod h1:3BSeJQF55QQI+dDFHx4WTIjTvX/Lvtk8Jaaf/SGAnBM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM= +github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= +github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 h1:vlfG6CqZ0wLdks8dzvNtuo8mBADO/RL0q5J2tseEI4s= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5/go.mod h1:+kRgiUXoVO3BX/VzeNadSCTXPXElAkPk1pNk1haKIDw= +github.com/daytona/clients/api-client-go v0.225.0 h1:C4Ucqync8uhSYkn6uVM7GJ2/O1rGWtJY9F916RRxGXg= +github.com/daytona/clients/api-client-go v0.225.0/go.mod h1:US+IQTDTZam9c4UpPMB3WDYYMIyWSKlgfDZzwWffY94= +github.com/daytona/clients/sdk-go v0.225.0 h1:C0zGXGtwYczb1b/MrX/Tc8YLQJuLQaztnu+u7Tz7i74= +github.com/daytona/clients/sdk-go v0.225.0/go.mod h1:TodsNlnrGObpupcLuxGl2F+kp7yyoFUTBt/CFaLJyqQ= +github.com/daytona/clients/toolbox-api-client-go v0.225.0 h1:tNrV4+lo970ZrNXdOVCR3yxpvRSh9LZL8OtzFupOkDs= +github.com/daytona/clients/toolbox-api-client-go v0.225.0/go.mod h1:lwZA3j823fNNiAgTSMThjZkYWegIigoFIREAtdo7o3E= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM= +go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/daytona/process/run-daytona-command/main.go b/examples/go/daytona/process/run-daytona-command/main.go new file mode 100644 index 00000000..76f1b175 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-command/main.go @@ -0,0 +1,52 @@ +// Run a command in a Daytona sandbox. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/daytona" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + config := &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.APIKey{ + ApiKey: &sandbox.APIKeyCredentials{ + Key: os.Getenv("DAYTONA_API_KEY"), + }, + }, + }, + Timeout: sandbox.Duration(2 * time.Minute), + } + if endpoint := os.Getenv("DAYTONA_API_URL"); endpoint != "" { + config.Endpoint = sandbox.Value(endpoint) + } + if target := os.Getenv("DAYTONA_TARGET"); target != "" { + config.Region = sandbox.Value(target) + } + client, err := sandbox.NewClient(daytona.New(), config) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Get(context.Background(), os.Getenv("DAYTONA_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d stdout=%q stderr=%q\n", result.GetExitCode(), string(result.GetStdout()), string(result.GetStderr())) +} diff --git a/examples/go/daytona/process/run-daytona-shell-command/.env.example b/examples/go/daytona/process/run-daytona-shell-command/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/go/daytona/process/run-daytona-shell-command/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/go/daytona/process/run-daytona-shell-command/README.md b/examples/go/daytona/process/run-daytona-shell-command/README.md new file mode 100644 index 00000000..9df8a2be --- /dev/null +++ b/examples/go/daytona/process/run-daytona-shell-command/README.md @@ -0,0 +1,53 @@ +# Run a Daytona shell command + +Run one shell string with `Process.Run` / `sandbox.process.run` using +`shell`. The program loads that sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- Set exactly one of `argv` or `shell`. Setting both, or neither, fails before + Daytona is called. The error field is `argv`. +- A blank `shell` fails with field `shell`. +- Omit or zero `timeout` so Daytona does not apply its 10-second native + default. Command timeouts must be whole seconds. Cancel the context to stop waiting. + +## Run + +```sh +cd examples/go/daytona/process/run-daytona-shell-command +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/daytona/process/run-daytona-shell-command/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/daytona/process/run-daytona-shell-command/go.mod b/examples/go/daytona/process/run-daytona-shell-command/go.mod new file mode 100644 index 00000000..eeb92296 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-shell-command/go.mod @@ -0,0 +1,71 @@ +module github.com/sandbox-kit/kit/examples/go/daytona/process/run-daytona-shell-command + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/daytona v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 // indirect + github.com/aws/smithy-go v1.28.5 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 // indirect + github.com/daytona/clients/api-client-go v0.225.0 // indirect + github.com/daytona/clients/sdk-go v0.225.0 // indirect + github.com/daytona/clients/toolbox-api-client-go v0.225.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/otel/sdk v1.47.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.47.0 // indirect + go.opentelemetry.io/otel/trace v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/daytona => ../../../../../sdks/go/providers/daytona + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/daytona/process/run-daytona-shell-command/go.sum b/examples/go/daytona/process/run-daytona-shell-command/go.sum new file mode 100644 index 00000000..d0a9e0cd --- /dev/null +++ b/examples/go/daytona/process/run-daytona-shell-command/go.sum @@ -0,0 +1,129 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= +github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ= +github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0= +github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 h1:q9YuNkYR/7lxflWCcEv3C0cecUt3Zvrh2hn9ZNxieNA= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16/go.mod h1:3BSeJQF55QQI+dDFHx4WTIjTvX/Lvtk8Jaaf/SGAnBM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM= +github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= +github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 h1:vlfG6CqZ0wLdks8dzvNtuo8mBADO/RL0q5J2tseEI4s= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5/go.mod h1:+kRgiUXoVO3BX/VzeNadSCTXPXElAkPk1pNk1haKIDw= +github.com/daytona/clients/api-client-go v0.225.0 h1:C4Ucqync8uhSYkn6uVM7GJ2/O1rGWtJY9F916RRxGXg= +github.com/daytona/clients/api-client-go v0.225.0/go.mod h1:US+IQTDTZam9c4UpPMB3WDYYMIyWSKlgfDZzwWffY94= +github.com/daytona/clients/sdk-go v0.225.0 h1:C0zGXGtwYczb1b/MrX/Tc8YLQJuLQaztnu+u7Tz7i74= +github.com/daytona/clients/sdk-go v0.225.0/go.mod h1:TodsNlnrGObpupcLuxGl2F+kp7yyoFUTBt/CFaLJyqQ= +github.com/daytona/clients/toolbox-api-client-go v0.225.0 h1:tNrV4+lo970ZrNXdOVCR3yxpvRSh9LZL8OtzFupOkDs= +github.com/daytona/clients/toolbox-api-client-go v0.225.0/go.mod h1:lwZA3j823fNNiAgTSMThjZkYWegIigoFIREAtdo7o3E= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM= +go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/daytona/process/run-daytona-shell-command/main.go b/examples/go/daytona/process/run-daytona-shell-command/main.go new file mode 100644 index 00000000..9d2831b6 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-shell-command/main.go @@ -0,0 +1,52 @@ +// Run a shell command in a Daytona sandbox. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/daytona" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + config := &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.APIKey{ + ApiKey: &sandbox.APIKeyCredentials{ + Key: os.Getenv("DAYTONA_API_KEY"), + }, + }, + }, + Timeout: sandbox.Duration(2 * time.Minute), + } + if endpoint := os.Getenv("DAYTONA_API_URL"); endpoint != "" { + config.Endpoint = sandbox.Value(endpoint) + } + if target := os.Getenv("DAYTONA_TARGET"); target != "" { + config.Region = sandbox.Value(target) + } + client, err := sandbox.NewClient(daytona.New(), config) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Get(context.Background(), os.Getenv("DAYTONA_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Shell: sandbox.Value(`echo "hello from shell"`), + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d stdout=%q\n", result.GetExitCode(), string(result.GetStdout())) +} diff --git a/examples/go/daytona/process/run-daytona-with-cwd/.env.example b/examples/go/daytona/process/run-daytona-with-cwd/.env.example new file mode 100644 index 00000000..a38c0bc4 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-cwd/.env.example @@ -0,0 +1,9 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +# Absolute working directory for this command. +DAYTONA_WORKING_DIRECTORY=/tmp diff --git a/examples/go/daytona/process/run-daytona-with-cwd/README.md b/examples/go/daytona/process/run-daytona-with-cwd/README.md new file mode 100644 index 00000000..e5b40318 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-cwd/README.md @@ -0,0 +1,54 @@ +# Run a Daytona command with a working directory + +Run `pwd` with an absolute `working_directory`. The program loads that +sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- A relative working directory is rejected before Daytona is called. The error + field is `working_directory`. +- Omit `working_directory` to use the Daytona sandbox default. + +## Run + +```sh +cd examples/go/daytona/process/run-daytona-with-cwd +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +# Absolute working directory for this command. +DAYTONA_WORKING_DIRECTORY=/tmp + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/daytona/process/run-daytona-with-cwd/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/daytona/process/run-daytona-with-cwd/go.mod b/examples/go/daytona/process/run-daytona-with-cwd/go.mod new file mode 100644 index 00000000..d577d4bd --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-cwd/go.mod @@ -0,0 +1,71 @@ +module github.com/sandbox-kit/kit/examples/go/daytona/process/run-daytona-with-cwd + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/daytona v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 // indirect + github.com/aws/smithy-go v1.28.5 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 // indirect + github.com/daytona/clients/api-client-go v0.225.0 // indirect + github.com/daytona/clients/sdk-go v0.225.0 // indirect + github.com/daytona/clients/toolbox-api-client-go v0.225.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/otel/sdk v1.47.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.47.0 // indirect + go.opentelemetry.io/otel/trace v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/daytona => ../../../../../sdks/go/providers/daytona + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/daytona/process/run-daytona-with-cwd/go.sum b/examples/go/daytona/process/run-daytona-with-cwd/go.sum new file mode 100644 index 00000000..d0a9e0cd --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-cwd/go.sum @@ -0,0 +1,129 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= +github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ= +github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0= +github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 h1:q9YuNkYR/7lxflWCcEv3C0cecUt3Zvrh2hn9ZNxieNA= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16/go.mod h1:3BSeJQF55QQI+dDFHx4WTIjTvX/Lvtk8Jaaf/SGAnBM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM= +github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= +github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 h1:vlfG6CqZ0wLdks8dzvNtuo8mBADO/RL0q5J2tseEI4s= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5/go.mod h1:+kRgiUXoVO3BX/VzeNadSCTXPXElAkPk1pNk1haKIDw= +github.com/daytona/clients/api-client-go v0.225.0 h1:C4Ucqync8uhSYkn6uVM7GJ2/O1rGWtJY9F916RRxGXg= +github.com/daytona/clients/api-client-go v0.225.0/go.mod h1:US+IQTDTZam9c4UpPMB3WDYYMIyWSKlgfDZzwWffY94= +github.com/daytona/clients/sdk-go v0.225.0 h1:C0zGXGtwYczb1b/MrX/Tc8YLQJuLQaztnu+u7Tz7i74= +github.com/daytona/clients/sdk-go v0.225.0/go.mod h1:TodsNlnrGObpupcLuxGl2F+kp7yyoFUTBt/CFaLJyqQ= +github.com/daytona/clients/toolbox-api-client-go v0.225.0 h1:tNrV4+lo970ZrNXdOVCR3yxpvRSh9LZL8OtzFupOkDs= +github.com/daytona/clients/toolbox-api-client-go v0.225.0/go.mod h1:lwZA3j823fNNiAgTSMThjZkYWegIigoFIREAtdo7o3E= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM= +go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/daytona/process/run-daytona-with-cwd/main.go b/examples/go/daytona/process/run-daytona-with-cwd/main.go new file mode 100644 index 00000000..ebc22245 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-cwd/main.go @@ -0,0 +1,57 @@ +// Run a Daytona command with a working directory. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/daytona" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + config := &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.APIKey{ + ApiKey: &sandbox.APIKeyCredentials{ + Key: os.Getenv("DAYTONA_API_KEY"), + }, + }, + }, + Timeout: sandbox.Duration(2 * time.Minute), + } + if endpoint := os.Getenv("DAYTONA_API_URL"); endpoint != "" { + config.Endpoint = sandbox.Value(endpoint) + } + if target := os.Getenv("DAYTONA_TARGET"); target != "" { + config.Region = sandbox.Value(target) + } + client, err := sandbox.NewClient(daytona.New(), config) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Get(context.Background(), os.Getenv("DAYTONA_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + cwd := os.Getenv("DAYTONA_WORKING_DIRECTORY") + if cwd == "" { + cwd = "/tmp" + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"pwd"}, + WorkingDirectory: sandbox.Value(cwd), + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d cwd_out=%q\n", result.GetExitCode(), string(result.GetStdout())) +} diff --git a/examples/go/daytona/process/run-daytona-with-env/.env.example b/examples/go/daytona/process/run-daytona-with-env/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-env/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/go/daytona/process/run-daytona-with-env/README.md b/examples/go/daytona/process/run-daytona-with-env/README.md new file mode 100644 index 00000000..e28671fe --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-env/README.md @@ -0,0 +1,51 @@ +# Run a Daytona command with environment variables + +Run `printenv KIT_DEMO` with environment variables for that call only. The +program loads that sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- An omitted `environment` map leaves the Daytona default. An empty map adds + no variables for this call. +- These variables apply to this command, not to later calls on the same handle. + +## Run + +```sh +cd examples/go/daytona/process/run-daytona-with-env +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/daytona/process/run-daytona-with-env/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/daytona/process/run-daytona-with-env/go.mod b/examples/go/daytona/process/run-daytona-with-env/go.mod new file mode 100644 index 00000000..0246b87a --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-env/go.mod @@ -0,0 +1,71 @@ +module github.com/sandbox-kit/kit/examples/go/daytona/process/run-daytona-with-env + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/daytona v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 // indirect + github.com/aws/smithy-go v1.28.5 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 // indirect + github.com/daytona/clients/api-client-go v0.225.0 // indirect + github.com/daytona/clients/sdk-go v0.225.0 // indirect + github.com/daytona/clients/toolbox-api-client-go v0.225.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/otel/sdk v1.47.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.47.0 // indirect + go.opentelemetry.io/otel/trace v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/daytona => ../../../../../sdks/go/providers/daytona + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/daytona/process/run-daytona-with-env/go.sum b/examples/go/daytona/process/run-daytona-with-env/go.sum new file mode 100644 index 00000000..d0a9e0cd --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-env/go.sum @@ -0,0 +1,129 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= +github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ= +github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0= +github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 h1:q9YuNkYR/7lxflWCcEv3C0cecUt3Zvrh2hn9ZNxieNA= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16/go.mod h1:3BSeJQF55QQI+dDFHx4WTIjTvX/Lvtk8Jaaf/SGAnBM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM= +github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= +github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 h1:vlfG6CqZ0wLdks8dzvNtuo8mBADO/RL0q5J2tseEI4s= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5/go.mod h1:+kRgiUXoVO3BX/VzeNadSCTXPXElAkPk1pNk1haKIDw= +github.com/daytona/clients/api-client-go v0.225.0 h1:C4Ucqync8uhSYkn6uVM7GJ2/O1rGWtJY9F916RRxGXg= +github.com/daytona/clients/api-client-go v0.225.0/go.mod h1:US+IQTDTZam9c4UpPMB3WDYYMIyWSKlgfDZzwWffY94= +github.com/daytona/clients/sdk-go v0.225.0 h1:C0zGXGtwYczb1b/MrX/Tc8YLQJuLQaztnu+u7Tz7i74= +github.com/daytona/clients/sdk-go v0.225.0/go.mod h1:TodsNlnrGObpupcLuxGl2F+kp7yyoFUTBt/CFaLJyqQ= +github.com/daytona/clients/toolbox-api-client-go v0.225.0 h1:tNrV4+lo970ZrNXdOVCR3yxpvRSh9LZL8OtzFupOkDs= +github.com/daytona/clients/toolbox-api-client-go v0.225.0/go.mod h1:lwZA3j823fNNiAgTSMThjZkYWegIigoFIREAtdo7o3E= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM= +go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/daytona/process/run-daytona-with-env/main.go b/examples/go/daytona/process/run-daytona-with-env/main.go new file mode 100644 index 00000000..4dc478c7 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-env/main.go @@ -0,0 +1,53 @@ +// Run a Daytona command with environment variables. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/daytona" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + config := &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.APIKey{ + ApiKey: &sandbox.APIKeyCredentials{ + Key: os.Getenv("DAYTONA_API_KEY"), + }, + }, + }, + Timeout: sandbox.Duration(2 * time.Minute), + } + if endpoint := os.Getenv("DAYTONA_API_URL"); endpoint != "" { + config.Endpoint = sandbox.Value(endpoint) + } + if target := os.Getenv("DAYTONA_TARGET"); target != "" { + config.Region = sandbox.Value(target) + } + client, err := sandbox.NewClient(daytona.New(), config) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Get(context.Background(), os.Getenv("DAYTONA_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"printenv", "KIT_DEMO"}, + Environment: map[string]string{"KIT_DEMO": "from-kit"}, + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d stdout=%q\n", result.GetExitCode(), string(result.GetStdout())) +} diff --git a/examples/go/daytona/process/run-daytona-with-timeout/.env.example b/examples/go/daytona/process/run-daytona-with-timeout/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-timeout/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/go/daytona/process/run-daytona-with-timeout/README.md b/examples/go/daytona/process/run-daytona-with-timeout/README.md new file mode 100644 index 00000000..086a1c86 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-timeout/README.md @@ -0,0 +1,55 @@ +# Cap a Daytona Process.Run timeout + +Run `echo hello with timeout` with `Process.Run` / `sandbox.process.run` and a +whole-second `timeout` of five seconds. The program loads that sandbox with +`client.get`. + +## What this means + +A positive `timeout` caps this call. Values must be whole seconds +(`5 * time.Second` / `5000` ms). Omit or zero adds no extra client deadline and +no separate Daytona command limit (Daytona would otherwise default to 10 +seconds). + +## Edge cases + +- `1500` ms / `1500 * time.Millisecond` is rejected before Daytona runs. The + error field is `timeout`. +- Cancel the Go context (or abort the TypeScript call) to end the wait. The + remote command may keep running; the handle stays busy until that provider + call settles. + +## Run + +```sh +cd examples/go/daytona/process/run-daytona-with-timeout +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/daytona/process/run-daytona-with-timeout/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/daytona/process/run-daytona-with-timeout/go.mod b/examples/go/daytona/process/run-daytona-with-timeout/go.mod new file mode 100644 index 00000000..4c1fc1d4 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-timeout/go.mod @@ -0,0 +1,71 @@ +module github.com/sandbox-kit/kit/examples/go/daytona/process/run-daytona-with-timeout + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/daytona v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 // indirect + github.com/aws/smithy-go v1.28.5 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 // indirect + github.com/daytona/clients/api-client-go v0.225.0 // indirect + github.com/daytona/clients/sdk-go v0.225.0 // indirect + github.com/daytona/clients/toolbox-api-client-go v0.225.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/otel/sdk v1.47.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.47.0 // indirect + go.opentelemetry.io/otel/trace v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/daytona => ../../../../../sdks/go/providers/daytona + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/daytona/process/run-daytona-with-timeout/go.sum b/examples/go/daytona/process/run-daytona-with-timeout/go.sum new file mode 100644 index 00000000..d0a9e0cd --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-timeout/go.sum @@ -0,0 +1,129 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= +github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ= +github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0= +github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16 h1:q9YuNkYR/7lxflWCcEv3C0cecUt3Zvrh2hn9ZNxieNA= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.4.16/go.mod h1:3BSeJQF55QQI+dDFHx4WTIjTvX/Lvtk8Jaaf/SGAnBM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM= +github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= +github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5 h1:vlfG6CqZ0wLdks8dzvNtuo8mBADO/RL0q5J2tseEI4s= +github.com/daytona/clients/analytics-api-client-go v0.0.0-20260722121532-3d2223c79fe5/go.mod h1:+kRgiUXoVO3BX/VzeNadSCTXPXElAkPk1pNk1haKIDw= +github.com/daytona/clients/api-client-go v0.225.0 h1:C4Ucqync8uhSYkn6uVM7GJ2/O1rGWtJY9F916RRxGXg= +github.com/daytona/clients/api-client-go v0.225.0/go.mod h1:US+IQTDTZam9c4UpPMB3WDYYMIyWSKlgfDZzwWffY94= +github.com/daytona/clients/sdk-go v0.225.0 h1:C0zGXGtwYczb1b/MrX/Tc8YLQJuLQaztnu+u7Tz7i74= +github.com/daytona/clients/sdk-go v0.225.0/go.mod h1:TodsNlnrGObpupcLuxGl2F+kp7yyoFUTBt/CFaLJyqQ= +github.com/daytona/clients/toolbox-api-client-go v0.225.0 h1:tNrV4+lo970ZrNXdOVCR3yxpvRSh9LZL8OtzFupOkDs= +github.com/daytona/clients/toolbox-api-client-go v0.225.0/go.mod h1:lwZA3j823fNNiAgTSMThjZkYWegIigoFIREAtdo7o3E= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM= +go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/daytona/process/run-daytona-with-timeout/main.go b/examples/go/daytona/process/run-daytona-with-timeout/main.go new file mode 100644 index 00000000..0ceb38b2 --- /dev/null +++ b/examples/go/daytona/process/run-daytona-with-timeout/main.go @@ -0,0 +1,53 @@ +// Cap a Daytona Process.Run with a whole-second timeout. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/daytona" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + config := &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.APIKey{ + ApiKey: &sandbox.APIKeyCredentials{ + Key: os.Getenv("DAYTONA_API_KEY"), + }, + }, + }, + Timeout: sandbox.Duration(2 * time.Minute), + } + if endpoint := os.Getenv("DAYTONA_API_URL"); endpoint != "" { + config.Endpoint = sandbox.Value(endpoint) + } + if target := os.Getenv("DAYTONA_TARGET"); target != "" { + config.Region = sandbox.Value(target) + } + client, err := sandbox.NewClient(daytona.New(), config) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Get(context.Background(), os.Getenv("DAYTONA_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello with timeout"}, + Timeout: sandbox.Duration(5 * time.Second), + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d stdout=%q stderr=%q\n", result.GetExitCode(), string(result.GetStdout()), string(result.GetStderr())) +} diff --git a/examples/go/modal/process/run-modal-check-exit/.env.example b/examples/go/modal/process/run-modal-check-exit/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/go/modal/process/run-modal-check-exit/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/go/modal/process/run-modal-check-exit/README.md b/examples/go/modal/process/run-modal-check-exit/README.md new file mode 100644 index 00000000..7f5d75ed --- /dev/null +++ b/examples/go/modal/process/run-modal-check-exit/README.md @@ -0,0 +1,49 @@ +# Check a non-zero Modal exit code + +Run `exit 7` via `shell` and read `exit_code` from the result. The program +loads that sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Modal returns both stdout and stderr. + +## Edge cases + +- A non-zero exit code is not an SDK error. Check `exit_code` / `exitCode` in + your program. +- Transport, validation, and Modal API failures still return errors. + +## Run + +```sh +cd examples/go/modal/process/run-modal-check-exit +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/modal/process/run-modal-check-exit/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/modal/process/run-modal-check-exit/go.mod b/examples/go/modal/process/run-modal-check-exit/go.mod new file mode 100644 index 00000000..4a823985 --- /dev/null +++ b/examples/go/modal/process/run-modal-check-exit/go.mod @@ -0,0 +1,45 @@ +module github.com/sandbox-kit/kit/examples/go/modal/process/run-modal-check-exit + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/modal v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kisielk/og-rek v1.3.0 // indirect + github.com/modal-labs/modal-client/go v0.11.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/modal => ../../../../../sdks/go/providers/modal + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/modal/process/run-modal-check-exit/go.sum b/examples/go/modal/process/run-modal-check-exit/go.sum new file mode 100644 index 00000000..3b03342e --- /dev/null +++ b/examples/go/modal/process/run-modal-check-exit/go.sum @@ -0,0 +1,80 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced h1:HxlRMDx/VeRqzj3nvqX9k4tjeBcEIkoNHDJPsS389hs= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced/go.mod h1:p7lmI+ecoe1RTyD11SPXWsSQ3H+pJ4cp5y7vtKW4QdM= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/og-rek v1.3.0 h1:lTXdQXqFETZKA//FWH4RBNAuiJ/dofxIwHAidoUZoMk= +github.com/kisielk/og-rek v1.3.0/go.mod h1:4at7oxyfBTDilURhNCf7irHWtosJlJl9uyqUqAkrP4w= +github.com/modal-labs/modal-client/go v0.11.1 h1:1ozfDwwgXsMqoVb2qsNdfo3ew6Ekq9Wo62dnS0oYwGc= +github.com/modal-labs/modal-client/go v0.11.1/go.mod h1:8jvKba8kejJuPZisuyYYl4n4kPhou+jEcg5w3XLymTo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/modal/process/run-modal-check-exit/main.go b/examples/go/modal/process/run-modal-check-exit/main.go new file mode 100644 index 00000000..5a271b60 --- /dev/null +++ b/examples/go/modal/process/run-modal-check-exit/main.go @@ -0,0 +1,57 @@ +// Check a non-zero exit code from a Modal command. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/modal" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + environment := os.Getenv("MODAL_ENVIRONMENT") + if environment == "" { + environment = "main" + } + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.TokenPair{ + TokenPair: &sandbox.TokenPairCredentials{ + Id: os.Getenv("MODAL_TOKEN_ID"), + Secret: os.Getenv("MODAL_TOKEN_SECRET"), + }, + }, + }, + Scope: &sandbox.Scope{ + AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME")), + Environment: sandbox.Value(environment), + }, + Timeout: sandbox.Duration(2 * time.Minute), + }) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Attach(context.Background(), os.Getenv("MODAL_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Shell: sandbox.Value("exit 7"), + }) + if err != nil { + log.Fatal(err) + } + if result.GetExitCode() != 7 { + log.Fatalf("want exit 7, got %d", result.GetExitCode()) + } + fmt.Printf("command finished with exit=%d\n", result.GetExitCode()) +} diff --git a/examples/go/modal/process/run-modal-command/.env.example b/examples/go/modal/process/run-modal-command/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/go/modal/process/run-modal-command/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/go/modal/process/run-modal-command/README.md b/examples/go/modal/process/run-modal-command/README.md new file mode 100644 index 00000000..06a9f5df --- /dev/null +++ b/examples/go/modal/process/run-modal-command/README.md @@ -0,0 +1,53 @@ +# Run a Modal command + +Run `echo hello from kit` with `Process.Run` / `sandbox.process.run` using +`argv`. The program opens that sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Modal returns both +stdout and stderr. + +## Edge cases + +- Set exactly one of `argv` or `shell`. Setting both, or neither, fails before + Modal is called. The error field is `argv`. +- A relative working directory is rejected before Modal is called. The error + field is `working_directory`. +- Omit or zero `Timeout` for no Modal command limit. Command timeouts must be + whole seconds. The error field is `timeout`. Cancel the context to stop waiting. + +## Run + +```sh +cd examples/go/modal/process/run-modal-command +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/modal/process/run-modal-command/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/modal/process/run-modal-command/go.mod b/examples/go/modal/process/run-modal-command/go.mod new file mode 100644 index 00000000..e1cf658a --- /dev/null +++ b/examples/go/modal/process/run-modal-command/go.mod @@ -0,0 +1,45 @@ +module github.com/sandbox-kit/kit/examples/go/modal/process/run-modal-command + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/modal v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kisielk/og-rek v1.3.0 // indirect + github.com/modal-labs/modal-client/go v0.11.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/modal => ../../../../../sdks/go/providers/modal + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/modal/process/run-modal-command/go.sum b/examples/go/modal/process/run-modal-command/go.sum new file mode 100644 index 00000000..3b03342e --- /dev/null +++ b/examples/go/modal/process/run-modal-command/go.sum @@ -0,0 +1,80 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced h1:HxlRMDx/VeRqzj3nvqX9k4tjeBcEIkoNHDJPsS389hs= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced/go.mod h1:p7lmI+ecoe1RTyD11SPXWsSQ3H+pJ4cp5y7vtKW4QdM= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/og-rek v1.3.0 h1:lTXdQXqFETZKA//FWH4RBNAuiJ/dofxIwHAidoUZoMk= +github.com/kisielk/og-rek v1.3.0/go.mod h1:4at7oxyfBTDilURhNCf7irHWtosJlJl9uyqUqAkrP4w= +github.com/modal-labs/modal-client/go v0.11.1 h1:1ozfDwwgXsMqoVb2qsNdfo3ew6Ekq9Wo62dnS0oYwGc= +github.com/modal-labs/modal-client/go v0.11.1/go.mod h1:8jvKba8kejJuPZisuyYYl4n4kPhou+jEcg5w3XLymTo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/modal/process/run-modal-command/main.go b/examples/go/modal/process/run-modal-command/main.go new file mode 100644 index 00000000..b91a974d --- /dev/null +++ b/examples/go/modal/process/run-modal-command/main.go @@ -0,0 +1,54 @@ +// Run a command in a Modal sandbox. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/modal" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + environment := os.Getenv("MODAL_ENVIRONMENT") + if environment == "" { + environment = "main" + } + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.TokenPair{ + TokenPair: &sandbox.TokenPairCredentials{ + Id: os.Getenv("MODAL_TOKEN_ID"), + Secret: os.Getenv("MODAL_TOKEN_SECRET"), + }, + }, + }, + Scope: &sandbox.Scope{ + AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME")), + Environment: sandbox.Value(environment), + }, + Timeout: sandbox.Duration(2 * time.Minute), + }) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Attach(context.Background(), os.Getenv("MODAL_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d stdout=%q stderr=%q\n", result.GetExitCode(), string(result.GetStdout()), string(result.GetStderr())) +} diff --git a/examples/go/modal/process/run-modal-shell-command/.env.example b/examples/go/modal/process/run-modal-shell-command/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/go/modal/process/run-modal-shell-command/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/go/modal/process/run-modal-shell-command/README.md b/examples/go/modal/process/run-modal-shell-command/README.md new file mode 100644 index 00000000..d9dfccf3 --- /dev/null +++ b/examples/go/modal/process/run-modal-shell-command/README.md @@ -0,0 +1,51 @@ +# Run a Modal shell command + +Run one shell string with `Process.Run` / `sandbox.process.run` using +`shell`. The program loads that sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Modal returns both stdout and stderr. + +## Edge cases + +- Set exactly one of `argv` or `shell`. Setting both, or neither, fails before + Modal is called. The error field is `argv`. +- A blank `shell` fails with field `shell`. `shell` runs as `bash -c`. +- Omit or zero `Timeout` for no Modal command limit. Command timeouts must be + whole seconds. + +## Run + +```sh +cd examples/go/modal/process/run-modal-shell-command +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/modal/process/run-modal-shell-command/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/modal/process/run-modal-shell-command/go.mod b/examples/go/modal/process/run-modal-shell-command/go.mod new file mode 100644 index 00000000..3854bbe3 --- /dev/null +++ b/examples/go/modal/process/run-modal-shell-command/go.mod @@ -0,0 +1,45 @@ +module github.com/sandbox-kit/kit/examples/go/modal/process/run-modal-shell-command + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/modal v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kisielk/og-rek v1.3.0 // indirect + github.com/modal-labs/modal-client/go v0.11.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/modal => ../../../../../sdks/go/providers/modal + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/modal/process/run-modal-shell-command/go.sum b/examples/go/modal/process/run-modal-shell-command/go.sum new file mode 100644 index 00000000..3b03342e --- /dev/null +++ b/examples/go/modal/process/run-modal-shell-command/go.sum @@ -0,0 +1,80 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced h1:HxlRMDx/VeRqzj3nvqX9k4tjeBcEIkoNHDJPsS389hs= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced/go.mod h1:p7lmI+ecoe1RTyD11SPXWsSQ3H+pJ4cp5y7vtKW4QdM= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/og-rek v1.3.0 h1:lTXdQXqFETZKA//FWH4RBNAuiJ/dofxIwHAidoUZoMk= +github.com/kisielk/og-rek v1.3.0/go.mod h1:4at7oxyfBTDilURhNCf7irHWtosJlJl9uyqUqAkrP4w= +github.com/modal-labs/modal-client/go v0.11.1 h1:1ozfDwwgXsMqoVb2qsNdfo3ew6Ekq9Wo62dnS0oYwGc= +github.com/modal-labs/modal-client/go v0.11.1/go.mod h1:8jvKba8kejJuPZisuyYYl4n4kPhou+jEcg5w3XLymTo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/modal/process/run-modal-shell-command/main.go b/examples/go/modal/process/run-modal-shell-command/main.go new file mode 100644 index 00000000..c0a4b102 --- /dev/null +++ b/examples/go/modal/process/run-modal-shell-command/main.go @@ -0,0 +1,54 @@ +// Run a shell command in a Modal sandbox. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/modal" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + environment := os.Getenv("MODAL_ENVIRONMENT") + if environment == "" { + environment = "main" + } + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.TokenPair{ + TokenPair: &sandbox.TokenPairCredentials{ + Id: os.Getenv("MODAL_TOKEN_ID"), + Secret: os.Getenv("MODAL_TOKEN_SECRET"), + }, + }, + }, + Scope: &sandbox.Scope{ + AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME")), + Environment: sandbox.Value(environment), + }, + Timeout: sandbox.Duration(2 * time.Minute), + }) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Attach(context.Background(), os.Getenv("MODAL_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Shell: sandbox.Value(`echo "hello from shell"`), + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d stdout=%q\n", result.GetExitCode(), string(result.GetStdout())) +} diff --git a/examples/go/modal/process/run-modal-with-cwd/.env.example b/examples/go/modal/process/run-modal-with-cwd/.env.example new file mode 100644 index 00000000..db4cbc20 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-cwd/.env.example @@ -0,0 +1,8 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +# Absolute working directory for this command. +MODAL_WORKING_DIRECTORY=/tmp diff --git a/examples/go/modal/process/run-modal-with-cwd/README.md b/examples/go/modal/process/run-modal-with-cwd/README.md new file mode 100644 index 00000000..ad4e84a4 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-cwd/README.md @@ -0,0 +1,52 @@ +# Run a Modal command with a working directory + +Run `pwd` with an absolute `working_directory`. The program loads that +sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Modal returns both stdout and stderr. + +## Edge cases + +- A relative working directory is rejected before Modal is called. The error + field is `working_directory`. +- Omit `working_directory` to use the Modal sandbox default. + +## Run + +```sh +cd examples/go/modal/process/run-modal-with-cwd +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +# Absolute working directory for this command. +MODAL_WORKING_DIRECTORY=/tmp + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/modal/process/run-modal-with-cwd/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/modal/process/run-modal-with-cwd/go.mod b/examples/go/modal/process/run-modal-with-cwd/go.mod new file mode 100644 index 00000000..1b24b801 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-cwd/go.mod @@ -0,0 +1,45 @@ +module github.com/sandbox-kit/kit/examples/go/modal/process/run-modal-with-cwd + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/modal v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kisielk/og-rek v1.3.0 // indirect + github.com/modal-labs/modal-client/go v0.11.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/modal => ../../../../../sdks/go/providers/modal + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/modal/process/run-modal-with-cwd/go.sum b/examples/go/modal/process/run-modal-with-cwd/go.sum new file mode 100644 index 00000000..3b03342e --- /dev/null +++ b/examples/go/modal/process/run-modal-with-cwd/go.sum @@ -0,0 +1,80 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced h1:HxlRMDx/VeRqzj3nvqX9k4tjeBcEIkoNHDJPsS389hs= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced/go.mod h1:p7lmI+ecoe1RTyD11SPXWsSQ3H+pJ4cp5y7vtKW4QdM= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/og-rek v1.3.0 h1:lTXdQXqFETZKA//FWH4RBNAuiJ/dofxIwHAidoUZoMk= +github.com/kisielk/og-rek v1.3.0/go.mod h1:4at7oxyfBTDilURhNCf7irHWtosJlJl9uyqUqAkrP4w= +github.com/modal-labs/modal-client/go v0.11.1 h1:1ozfDwwgXsMqoVb2qsNdfo3ew6Ekq9Wo62dnS0oYwGc= +github.com/modal-labs/modal-client/go v0.11.1/go.mod h1:8jvKba8kejJuPZisuyYYl4n4kPhou+jEcg5w3XLymTo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/modal/process/run-modal-with-cwd/main.go b/examples/go/modal/process/run-modal-with-cwd/main.go new file mode 100644 index 00000000..7ba4ece9 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-cwd/main.go @@ -0,0 +1,59 @@ +// Run a Modal command with a working directory. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/modal" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + environment := os.Getenv("MODAL_ENVIRONMENT") + if environment == "" { + environment = "main" + } + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.TokenPair{ + TokenPair: &sandbox.TokenPairCredentials{ + Id: os.Getenv("MODAL_TOKEN_ID"), + Secret: os.Getenv("MODAL_TOKEN_SECRET"), + }, + }, + }, + Scope: &sandbox.Scope{ + AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME")), + Environment: sandbox.Value(environment), + }, + Timeout: sandbox.Duration(2 * time.Minute), + }) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Attach(context.Background(), os.Getenv("MODAL_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + cwd := os.Getenv("MODAL_WORKING_DIRECTORY") + if cwd == "" { + cwd = "/tmp" + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"pwd"}, + WorkingDirectory: sandbox.Value(cwd), + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d cwd_out=%q\n", result.GetExitCode(), string(result.GetStdout())) +} diff --git a/examples/go/modal/process/run-modal-with-env/.env.example b/examples/go/modal/process/run-modal-with-env/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-env/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/go/modal/process/run-modal-with-env/README.md b/examples/go/modal/process/run-modal-with-env/README.md new file mode 100644 index 00000000..c35dd92c --- /dev/null +++ b/examples/go/modal/process/run-modal-with-env/README.md @@ -0,0 +1,49 @@ +# Run a Modal command with environment variables + +Run `printenv KIT_DEMO` with environment variables for that call only. The +program loads that sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Modal returns both stdout and stderr. + +## Edge cases + +- An omitted `environment` map leaves the Modal default. An empty map adds + no variables for this call. +- These variables apply to this command, not to later calls on the same handle. + +## Run + +```sh +cd examples/go/modal/process/run-modal-with-env +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/modal/process/run-modal-with-env/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/modal/process/run-modal-with-env/go.mod b/examples/go/modal/process/run-modal-with-env/go.mod new file mode 100644 index 00000000..fec44f28 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-env/go.mod @@ -0,0 +1,45 @@ +module github.com/sandbox-kit/kit/examples/go/modal/process/run-modal-with-env + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/modal v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kisielk/og-rek v1.3.0 // indirect + github.com/modal-labs/modal-client/go v0.11.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/modal => ../../../../../sdks/go/providers/modal + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/modal/process/run-modal-with-env/go.sum b/examples/go/modal/process/run-modal-with-env/go.sum new file mode 100644 index 00000000..3b03342e --- /dev/null +++ b/examples/go/modal/process/run-modal-with-env/go.sum @@ -0,0 +1,80 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced h1:HxlRMDx/VeRqzj3nvqX9k4tjeBcEIkoNHDJPsS389hs= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced/go.mod h1:p7lmI+ecoe1RTyD11SPXWsSQ3H+pJ4cp5y7vtKW4QdM= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/og-rek v1.3.0 h1:lTXdQXqFETZKA//FWH4RBNAuiJ/dofxIwHAidoUZoMk= +github.com/kisielk/og-rek v1.3.0/go.mod h1:4at7oxyfBTDilURhNCf7irHWtosJlJl9uyqUqAkrP4w= +github.com/modal-labs/modal-client/go v0.11.1 h1:1ozfDwwgXsMqoVb2qsNdfo3ew6Ekq9Wo62dnS0oYwGc= +github.com/modal-labs/modal-client/go v0.11.1/go.mod h1:8jvKba8kejJuPZisuyYYl4n4kPhou+jEcg5w3XLymTo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/modal/process/run-modal-with-env/main.go b/examples/go/modal/process/run-modal-with-env/main.go new file mode 100644 index 00000000..f6c0f142 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-env/main.go @@ -0,0 +1,55 @@ +// Run a Modal command with environment variables. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/modal" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + environment := os.Getenv("MODAL_ENVIRONMENT") + if environment == "" { + environment = "main" + } + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.TokenPair{ + TokenPair: &sandbox.TokenPairCredentials{ + Id: os.Getenv("MODAL_TOKEN_ID"), + Secret: os.Getenv("MODAL_TOKEN_SECRET"), + }, + }, + }, + Scope: &sandbox.Scope{ + AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME")), + Environment: sandbox.Value(environment), + }, + Timeout: sandbox.Duration(2 * time.Minute), + }) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Attach(context.Background(), os.Getenv("MODAL_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"printenv", "KIT_DEMO"}, + Environment: map[string]string{"KIT_DEMO": "from-kit"}, + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d stdout=%q\n", result.GetExitCode(), string(result.GetStdout())) +} diff --git a/examples/go/modal/process/run-modal-with-timeout/.env.example b/examples/go/modal/process/run-modal-with-timeout/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-timeout/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/go/modal/process/run-modal-with-timeout/README.md b/examples/go/modal/process/run-modal-with-timeout/README.md new file mode 100644 index 00000000..6adb3045 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-timeout/README.md @@ -0,0 +1,53 @@ +# Cap a Modal Process.Run timeout + +Run `echo hello with timeout` with `Process.Run` / `sandbox.process.run` and a +whole-second `timeout` of five seconds. The program loads that sandbox with +`client.attach`. + +## What this means + +A positive `timeout` caps this call. Values must be whole seconds +(`5 * time.Second` / `5000` ms). Omit or zero adds no Modal command limit (Go +sends `Timeout: 0`; TypeScript omits `timeoutMs`). + +## Edge cases + +- `1500` ms / `1500 * time.Millisecond` is rejected before Modal runs. The + error field is `timeout`. +- Cancel the Go context (or abort the TypeScript call) to end the wait. The + remote command may keep running; the handle stays busy until that provider + call settles. + +## Run + +```sh +cd examples/go/modal/process/run-modal-with-timeout +cp .env.example .env +# Fill in the values below, then: +go run . +``` + +Requires Go 1.26.0+ and this checkout. Skip copying if `.env` already exists. +Local check: `GOWORK=off go test ./...`. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [typescript twin](../../../../typescript/modal/process/run-modal-with-timeout/README.md) +- Catalog: [examples/go/README.md](../../../README.md) diff --git a/examples/go/modal/process/run-modal-with-timeout/go.mod b/examples/go/modal/process/run-modal-with-timeout/go.mod new file mode 100644 index 00000000..bd020433 --- /dev/null +++ b/examples/go/modal/process/run-modal-with-timeout/go.mod @@ -0,0 +1,45 @@ +module github.com/sandbox-kit/kit/examples/go/modal/process/run-modal-with-timeout + +go 1.26.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/sandbox-kit/kit/sdks/go/providers/modal v0.1.4 + github.com/sandbox-kit/kit/sdks/go/sandbox v0.1.4 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kisielk/og-rek v1.3.0 // indirect + github.com/modal-labs/modal-client/go v0.11.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/contracts v0.1.4 // indirect + github.com/sandbox-kit/kit/sdks/go/internal/sdk v0.1.4 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) + +replace github.com/sandbox-kit/kit/sdks/go/sandbox => ../../../../../sdks/go/sandbox + +replace github.com/sandbox-kit/kit/sdks/go/providers/modal => ../../../../../sdks/go/providers/modal + +replace github.com/sandbox-kit/kit/sdks/go/internal/contracts => ../../../../../sdks/go/internal/contracts + +replace github.com/sandbox-kit/kit/sdks/go/internal/sdk => ../../../../../sdks/go/internal/sdk diff --git a/examples/go/modal/process/run-modal-with-timeout/go.sum b/examples/go/modal/process/run-modal-with-timeout/go.sum new file mode 100644 index 00000000..3b03342e --- /dev/null +++ b/examples/go/modal/process/run-modal-with-timeout/go.sum @@ -0,0 +1,80 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced h1:HxlRMDx/VeRqzj3nvqX9k4tjeBcEIkoNHDJPsS389hs= +github.com/aristanetworks/gomap v0.0.0-20230726210543-f4e41046dced/go.mod h1:p7lmI+ecoe1RTyD11SPXWsSQ3H+pJ4cp5y7vtKW4QdM= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/og-rek v1.3.0 h1:lTXdQXqFETZKA//FWH4RBNAuiJ/dofxIwHAidoUZoMk= +github.com/kisielk/og-rek v1.3.0/go.mod h1:4at7oxyfBTDilURhNCf7irHWtosJlJl9uyqUqAkrP4w= +github.com/modal-labs/modal-client/go v0.11.1 h1:1ozfDwwgXsMqoVb2qsNdfo3ew6Ekq9Wo62dnS0oYwGc= +github.com/modal-labs/modal-client/go v0.11.1/go.mod h1:8jvKba8kejJuPZisuyYYl4n4kPhou+jEcg5w3XLymTo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e h1:01Ju2A/fZKkci4zqx0eZxw//DnRYOnBiGJG14hFBhO8= +golang.org/x/exp v0.0.0-20260820142414-ca536658362e/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= +google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/examples/go/modal/process/run-modal-with-timeout/main.go b/examples/go/modal/process/run-modal-with-timeout/main.go new file mode 100644 index 00000000..34bda60f --- /dev/null +++ b/examples/go/modal/process/run-modal-with-timeout/main.go @@ -0,0 +1,55 @@ +// Cap a Modal Process.Run with a whole-second timeout. +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/sandbox-kit/kit/sdks/go/providers/modal" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("copy .env.example to .env and fill in its values: %v", err) + } + environment := os.Getenv("MODAL_ENVIRONMENT") + if environment == "" { + environment = "main" + } + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Auth: &sandbox.AuthConfig{ + Credentials: &sandbox.TokenPair{ + TokenPair: &sandbox.TokenPairCredentials{ + Id: os.Getenv("MODAL_TOKEN_ID"), + Secret: os.Getenv("MODAL_TOKEN_SECRET"), + }, + }, + }, + Scope: &sandbox.Scope{ + AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME")), + Environment: sandbox.Value(environment), + }, + Timeout: sandbox.Duration(2 * time.Minute), + }) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + instance, err := client.Attach(context.Background(), os.Getenv("MODAL_SANDBOX_ID")) + if err != nil { + log.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello with timeout"}, + Timeout: sandbox.Duration(5 * time.Second), + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("exit=%d stdout=%q stderr=%q\n", result.GetExitCode(), string(result.GetStdout()), string(result.GetStderr())) +} diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 274a01b0..2e3e90b1 100644 --- a/examples/typescript/README.md +++ b/examples/typescript/README.md @@ -1,9 +1,9 @@ # TypeScript examples Independent Node.js projects. Each selects one provider and demonstrates one -operation. Projects sit in `filesystem`, `configuration`, `lifecycle`, or -`errors`. Requires Node.js 24.15.0 (or Node.js 20/22), npm 11.19.0, and this -checkout. +operation. Projects sit in `filesystem`, `process`, `configuration`, +`lifecycle`, or `errors`. Requires Node.js 24.15.0 (or Node.js 20/22), npm +11.19.0, and this checkout. Examples use the public `@sandbox-kit/sandbox` API: nested exclusive fields (`auth: { apiKey: … }`, `source: { image: … }`), millisecond timeouts, and plain @@ -20,7 +20,7 @@ npm run build npm start ``` -Creation examples provision real cloud resources. Filesystem and lifecycle +Creation examples provision real cloud resources. Filesystem, process, and lifecycle examples act on the sandbox ID in `.env`. Closing a handle leaves the sandbox as it is. Closing the client closes your connection to Daytona or Modal. Modal setup: [create-modal-sandbox](modal/configuration/create-modal-sandbox/README.md). @@ -32,6 +32,12 @@ the client closes your connection to Daytona or Modal. Modal setup: | Project | Demonstrates | | -------------------------------------------------------------------------------------------- | --------------------------------------- | | [create-daytona-sandbox](daytona/configuration/create-daytona-sandbox/README.md) | default Daytona snapshot | +| [run-daytona-command](daytona/process/run-daytona-command/README.md) | run one command | +| [run-daytona-shell-command](daytona/process/run-daytona-shell-command/README.md) | run one shell string | +| [run-daytona-with-cwd](daytona/process/run-daytona-with-cwd/README.md) | run with working directory | +| [run-daytona-with-env](daytona/process/run-daytona-with-env/README.md) | run with environment variables | +| [run-daytona-with-timeout](daytona/process/run-daytona-with-timeout/README.md) | run with a whole-second timeout | +| [run-daytona-check-exit](daytona/process/run-daytona-check-exit/README.md) | non-zero exit in the result | | [read-daytona-file](daytona/filesystem/read-daytona-file/README.md) | read one file | | [write-daytona-file](daytona/filesystem/write-daytona-file/README.md) | write one file | | [copy-file-into-daytona](daytona/filesystem/copy-file-into-daytona/README.md) | copy a local file in | @@ -69,6 +75,12 @@ the client closes your connection to Daytona or Modal. Modal setup: | Project | Demonstrates | | ------------------------------------------------------------------------------------------ | ------------------------------------------ | | [create-modal-sandbox](modal/configuration/create-modal-sandbox/README.md) | image creation with resources and policies | +| [run-modal-command](modal/process/run-modal-command/README.md) | run one command | +| [run-modal-shell-command](modal/process/run-modal-shell-command/README.md) | run one shell string | +| [run-modal-with-cwd](modal/process/run-modal-with-cwd/README.md) | run with working directory | +| [run-modal-with-env](modal/process/run-modal-with-env/README.md) | run with environment variables | +| [run-modal-with-timeout](modal/process/run-modal-with-timeout/README.md) | run with a whole-second timeout | +| [run-modal-check-exit](modal/process/run-modal-check-exit/README.md) | non-zero exit in the result | | [read-modal-file](modal/filesystem/read-modal-file/README.md) | read one file | | [write-modal-file](modal/filesystem/write-modal-file/README.md) | write one file | | [copy-file-into-modal](modal/filesystem/copy-file-into-modal/README.md) | copy a local file in | diff --git a/examples/typescript/daytona/process/run-daytona-check-exit/.env.example b/examples/typescript/daytona/process/run-daytona-check-exit/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-check-exit/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/typescript/daytona/process/run-daytona-check-exit/.gitignore b/examples/typescript/daytona/process/run-daytona-check-exit/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-check-exit/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/daytona/process/run-daytona-check-exit/README.md b/examples/typescript/daytona/process/run-daytona-check-exit/README.md new file mode 100644 index 00000000..a195dbe2 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-check-exit/README.md @@ -0,0 +1,50 @@ +# Check a non-zero Daytona exit code + +Run `exit 7` via `shell` and read `exit_code` from the result. The program +loads that sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- A non-zero exit code is not an SDK error. Check `exit_code` / `exitCode` in + your program. +- Transport, validation, and Daytona API failures still return errors. + +## Run + +```sh +cd examples/typescript/daytona/process/run-daytona-check-exit +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/daytona/process/run-daytona-check-exit/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/daytona/process/run-daytona-check-exit/package-lock.json b/examples/typescript/daytona/process/run-daytona-check-exit/package-lock.json new file mode 100644 index 00000000..1d6e73ef --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-check-exit/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-daytona-check-exit", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-daytona-check-exit", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/daytona": { + "name": "@sandbox-kit/daytona", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@daytona/sdk": "0.225.0", + "@sandbox-kit/sandbox": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/daytona": { + "resolved": "../../../../../sdks/typescript/providers/daytona", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/daytona/process/run-daytona-check-exit/package.json b/examples/typescript/daytona/process/run-daytona-check-exit/package.json new file mode 100644 index 00000000..da91fee1 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-check-exit/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-daytona-check-exit", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/daytona/process/run-daytona-check-exit/src/main.ts b/examples/typescript/daytona/process/run-daytona-check-exit/src/main.ts new file mode 100644 index 00000000..86dba080 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-check-exit/src/main.ts @@ -0,0 +1,19 @@ +import { daytona } from "@sandbox-kit/daytona"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: daytona(), + auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } }, + timeout: 120_000, + ...(process.env.DAYTONA_API_URL ? { endpoint: process.env.DAYTONA_API_URL } : {}), + ...(process.env.DAYTONA_TARGET ? { region: process.env.DAYTONA_TARGET } : {}), +}); + +const sandbox = await client.get(process.env.DAYTONA_SANDBOX_ID!); +const result = await sandbox.process.run({ + shell: "exit 7", +}); +if (result.exitCode !== 7) { + throw new Error(`want exit 7, got ${result.exitCode}`); +} +console.log(`command finished with exit=${result.exitCode}`); diff --git a/examples/typescript/daytona/process/run-daytona-check-exit/tsconfig.json b/examples/typescript/daytona/process/run-daytona-check-exit/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-check-exit/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/examples/typescript/daytona/process/run-daytona-command/.env.example b/examples/typescript/daytona/process/run-daytona-command/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-command/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/typescript/daytona/process/run-daytona-command/.gitignore b/examples/typescript/daytona/process/run-daytona-command/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-command/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/daytona/process/run-daytona-command/README.md b/examples/typescript/daytona/process/run-daytona-command/README.md new file mode 100644 index 00000000..3d1b0b70 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-command/README.md @@ -0,0 +1,56 @@ +# Run a Daytona command + +Run `echo hello from kit` with `sandbox.process.run` using `argv`. The program +loads that sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exitCode`, `stdout`, and +`stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- Set exactly one of `argv` or `shell`. Setting both, or neither, fails before + Daytona is called. The error field is `argv`. +- A relative working directory is rejected before Daytona is called. The error + field is `working_directory`. +- Omit or zero `timeout` so Daytona does not apply its 10-second native + default. Command timeouts must be whole seconds (`2000` is valid; `1500` is + rejected). The error field is `timeout`. Aborting the signal rejects the + await; the remote command may keep running. + +## Run + +```sh +cd examples/typescript/daytona/process/run-daytona-command +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already +exists. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [go twin](../../../../go/daytona/process/run-daytona-command/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/daytona/process/run-daytona-command/package-lock.json b/examples/typescript/daytona/process/run-daytona-command/package-lock.json new file mode 100644 index 00000000..dedcd499 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-command/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-daytona-command", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-daytona-command", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/daytona": { + "name": "@sandbox-kit/daytona", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@daytona/sdk": "0.225.0", + "@sandbox-kit/sandbox": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/daytona": { + "resolved": "../../../../../sdks/typescript/providers/daytona", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/daytona/process/run-daytona-command/package.json b/examples/typescript/daytona/process/run-daytona-command/package.json new file mode 100644 index 00000000..dfd4c4f9 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-command/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-daytona-command", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/daytona/process/run-daytona-command/src/main.ts b/examples/typescript/daytona/process/run-daytona-command/src/main.ts new file mode 100644 index 00000000..0cad2ade --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-command/src/main.ts @@ -0,0 +1,18 @@ +import { daytona } from "@sandbox-kit/daytona"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: daytona(), + auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } }, + timeout: 120_000, + ...(process.env.DAYTONA_API_URL ? { endpoint: process.env.DAYTONA_API_URL } : {}), + ...(process.env.DAYTONA_TARGET ? { region: process.env.DAYTONA_TARGET } : {}), +}); + +const sandbox = await client.get(process.env.DAYTONA_SANDBOX_ID!); +const result = await sandbox.process.run({ + argv: ["echo", "hello from kit"], +}); +console.log( + `exit=${result.exitCode} stdout=${JSON.stringify(new TextDecoder().decode(result.stdout))} stderr=${JSON.stringify(new TextDecoder().decode(result.stderr))}`, +); diff --git a/examples/typescript/daytona/process/run-daytona-command/tsconfig.json b/examples/typescript/daytona/process/run-daytona-command/tsconfig.json new file mode 100644 index 00000000..b1e6e13e --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-command/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM", "ESNext.Disposable"], + "types": ["node"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/examples/typescript/daytona/process/run-daytona-shell-command/.env.example b/examples/typescript/daytona/process/run-daytona-shell-command/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-shell-command/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/typescript/daytona/process/run-daytona-shell-command/.gitignore b/examples/typescript/daytona/process/run-daytona-shell-command/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-shell-command/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/daytona/process/run-daytona-shell-command/README.md b/examples/typescript/daytona/process/run-daytona-shell-command/README.md new file mode 100644 index 00000000..4eae56cb --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-shell-command/README.md @@ -0,0 +1,52 @@ +# Run a Daytona shell command + +Run one shell string with `Process.Run` / `sandbox.process.run` using +`shell`. The program loads that sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- Set exactly one of `argv` or `shell`. Setting both, or neither, fails before + Daytona is called. The error field is `argv`. +- A blank `shell` fails with field `shell`. +- Omit or zero `timeout` so Daytona does not apply its 10-second native + default. Command timeouts must be whole seconds. + +## Run + +```sh +cd examples/typescript/daytona/process/run-daytona-shell-command +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/daytona/process/run-daytona-shell-command/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/daytona/process/run-daytona-shell-command/package-lock.json b/examples/typescript/daytona/process/run-daytona-shell-command/package-lock.json new file mode 100644 index 00000000..071a38fb --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-shell-command/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-daytona-shell-command", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-daytona-shell-command", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/daytona": { + "name": "@sandbox-kit/daytona", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@daytona/sdk": "0.225.0", + "@sandbox-kit/sandbox": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/daytona": { + "resolved": "../../../../../sdks/typescript/providers/daytona", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/daytona/process/run-daytona-shell-command/package.json b/examples/typescript/daytona/process/run-daytona-shell-command/package.json new file mode 100644 index 00000000..f73d40fa --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-shell-command/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-daytona-shell-command", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/daytona/process/run-daytona-shell-command/src/main.ts b/examples/typescript/daytona/process/run-daytona-shell-command/src/main.ts new file mode 100644 index 00000000..14ebb5ee --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-shell-command/src/main.ts @@ -0,0 +1,16 @@ +import { daytona } from "@sandbox-kit/daytona"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: daytona(), + auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } }, + timeout: 120_000, + ...(process.env.DAYTONA_API_URL ? { endpoint: process.env.DAYTONA_API_URL } : {}), + ...(process.env.DAYTONA_TARGET ? { region: process.env.DAYTONA_TARGET } : {}), +}); + +const sandbox = await client.get(process.env.DAYTONA_SANDBOX_ID!); +const result = await sandbox.process.run({ + shell: 'echo "hello from shell"', +}); +console.log(`exit=${result.exitCode} stdout=${JSON.stringify(new TextDecoder().decode(result.stdout))}`); diff --git a/examples/typescript/daytona/process/run-daytona-shell-command/tsconfig.json b/examples/typescript/daytona/process/run-daytona-shell-command/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-shell-command/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/examples/typescript/daytona/process/run-daytona-with-cwd/.env.example b/examples/typescript/daytona/process/run-daytona-with-cwd/.env.example new file mode 100644 index 00000000..a38c0bc4 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-cwd/.env.example @@ -0,0 +1,9 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +# Absolute working directory for this command. +DAYTONA_WORKING_DIRECTORY=/tmp diff --git a/examples/typescript/daytona/process/run-daytona-with-cwd/.gitignore b/examples/typescript/daytona/process/run-daytona-with-cwd/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-cwd/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/daytona/process/run-daytona-with-cwd/README.md b/examples/typescript/daytona/process/run-daytona-with-cwd/README.md new file mode 100644 index 00000000..2f969419 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-cwd/README.md @@ -0,0 +1,53 @@ +# Run a Daytona command with a working directory + +Run `pwd` with an absolute `working_directory`. The program loads that +sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- A relative working directory is rejected before Daytona is called. The error + field is `working_directory`. +- Omit `working_directory` to use the Daytona sandbox default. + +## Run + +```sh +cd examples/typescript/daytona/process/run-daytona-with-cwd +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +# Absolute working directory for this command. +DAYTONA_WORKING_DIRECTORY=/tmp + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/daytona/process/run-daytona-with-cwd/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/daytona/process/run-daytona-with-cwd/package-lock.json b/examples/typescript/daytona/process/run-daytona-with-cwd/package-lock.json new file mode 100644 index 00000000..0156440c --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-cwd/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-daytona-with-cwd", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-daytona-with-cwd", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/daytona": { + "name": "@sandbox-kit/daytona", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@daytona/sdk": "0.225.0", + "@sandbox-kit/sandbox": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/daytona": { + "resolved": "../../../../../sdks/typescript/providers/daytona", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/daytona/process/run-daytona-with-cwd/package.json b/examples/typescript/daytona/process/run-daytona-with-cwd/package.json new file mode 100644 index 00000000..dc94c11d --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-cwd/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-daytona-with-cwd", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/daytona/process/run-daytona-with-cwd/src/main.ts b/examples/typescript/daytona/process/run-daytona-with-cwd/src/main.ts new file mode 100644 index 00000000..cce7a488 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-cwd/src/main.ts @@ -0,0 +1,18 @@ +import { daytona } from "@sandbox-kit/daytona"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: daytona(), + auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } }, + timeout: 120_000, + ...(process.env.DAYTONA_API_URL ? { endpoint: process.env.DAYTONA_API_URL } : {}), + ...(process.env.DAYTONA_TARGET ? { region: process.env.DAYTONA_TARGET } : {}), +}); + +const sandbox = await client.get(process.env.DAYTONA_SANDBOX_ID!); +const cwd = process.env.DAYTONA_WORKING_DIRECTORY || "/tmp"; +const result = await sandbox.process.run({ + argv: ["pwd"], + workingDirectory: cwd, +}); +console.log(`exit=${result.exitCode} cwd_out=${JSON.stringify(new TextDecoder().decode(result.stdout))}`); diff --git a/examples/typescript/daytona/process/run-daytona-with-cwd/tsconfig.json b/examples/typescript/daytona/process/run-daytona-with-cwd/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-cwd/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/examples/typescript/daytona/process/run-daytona-with-env/.env.example b/examples/typescript/daytona/process/run-daytona-with-env/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-env/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/typescript/daytona/process/run-daytona-with-env/.gitignore b/examples/typescript/daytona/process/run-daytona-with-env/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-env/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/daytona/process/run-daytona-with-env/README.md b/examples/typescript/daytona/process/run-daytona-with-env/README.md new file mode 100644 index 00000000..83bb77cd --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-env/README.md @@ -0,0 +1,50 @@ +# Run a Daytona command with environment variables + +Run `printenv KIT_DEMO` with environment variables for that call only. The +program loads that sandbox with `client.get`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Daytona's basic exec +returns stdout only; `stderr` is empty. + +## Edge cases + +- An omitted `environment` map leaves the Daytona default. An empty map adds + no variables for this call. +- These variables apply to this command, not to later calls on the same handle. + +## Run + +```sh +cd examples/typescript/daytona/process/run-daytona-with-env +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/daytona/process/run-daytona-with-env/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/daytona/process/run-daytona-with-env/package-lock.json b/examples/typescript/daytona/process/run-daytona-with-env/package-lock.json new file mode 100644 index 00000000..81114301 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-env/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-daytona-with-env", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-daytona-with-env", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/daytona": { + "name": "@sandbox-kit/daytona", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@daytona/sdk": "0.225.0", + "@sandbox-kit/sandbox": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/daytona": { + "resolved": "../../../../../sdks/typescript/providers/daytona", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/daytona/process/run-daytona-with-env/package.json b/examples/typescript/daytona/process/run-daytona-with-env/package.json new file mode 100644 index 00000000..8f89b94b --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-env/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-daytona-with-env", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/daytona/process/run-daytona-with-env/src/main.ts b/examples/typescript/daytona/process/run-daytona-with-env/src/main.ts new file mode 100644 index 00000000..51a01f2e --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-env/src/main.ts @@ -0,0 +1,17 @@ +import { daytona } from "@sandbox-kit/daytona"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: daytona(), + auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } }, + timeout: 120_000, + ...(process.env.DAYTONA_API_URL ? { endpoint: process.env.DAYTONA_API_URL } : {}), + ...(process.env.DAYTONA_TARGET ? { region: process.env.DAYTONA_TARGET } : {}), +}); + +const sandbox = await client.get(process.env.DAYTONA_SANDBOX_ID!); +const result = await sandbox.process.run({ + argv: ["printenv", "KIT_DEMO"], + environment: { KIT_DEMO: "from-kit" }, +}); +console.log(`exit=${result.exitCode} stdout=${JSON.stringify(new TextDecoder().decode(result.stdout))}`); diff --git a/examples/typescript/daytona/process/run-daytona-with-env/tsconfig.json b/examples/typescript/daytona/process/run-daytona-with-env/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-env/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/examples/typescript/daytona/process/run-daytona-with-timeout/.env.example b/examples/typescript/daytona/process/run-daytona-with-timeout/.env.example new file mode 100644 index 00000000..0b499dca --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-timeout/.env.example @@ -0,0 +1,6 @@ +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= diff --git a/examples/typescript/daytona/process/run-daytona-with-timeout/.gitignore b/examples/typescript/daytona/process/run-daytona-with-timeout/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-timeout/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/daytona/process/run-daytona-with-timeout/README.md b/examples/typescript/daytona/process/run-daytona-with-timeout/README.md new file mode 100644 index 00000000..fa3b73ef --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-timeout/README.md @@ -0,0 +1,53 @@ +# Cap a Daytona Process.Run timeout + +Run `echo hello with timeout` with `sandbox.process.run` / `Process.Run` and a +whole-second `timeout` of five seconds (`5000` ms). The program loads that +sandbox with `client.get`. + +## What this means + +A positive `timeout` caps this call. Values must be whole seconds +(`5000` ms / `5 * time.Second`). Omit or zero adds no extra client deadline and +no separate Daytona command limit (Daytona would otherwise default to 10 +seconds). + +## Edge cases + +- `1500` ms is rejected before Daytona runs. The error field is `timeout`. +- Abort the call (or cancel the Go context) to end the wait. The remote + command may keep running; the handle stays busy until that provider call + settles. + +## Run + +```sh +cd examples/typescript/daytona/process/run-daytona-with-timeout +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +DAYTONA_API_KEY= +DAYTONA_SANDBOX_ID= + +# Optional Daytona endpoint and target overrides. +# DAYTONA_API_URL=https://app.daytona.io/api +# DAYTONA_TARGET= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/daytona/process/run-daytona-with-timeout/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/daytona/process/run-daytona-with-timeout/package-lock.json b/examples/typescript/daytona/process/run-daytona-with-timeout/package-lock.json new file mode 100644 index 00000000..486938e6 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-timeout/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-daytona-with-timeout", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-daytona-with-timeout", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/daytona": { + "name": "@sandbox-kit/daytona", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@daytona/sdk": "0.225.0", + "@sandbox-kit/sandbox": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/daytona": { + "resolved": "../../../../../sdks/typescript/providers/daytona", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/daytona/process/run-daytona-with-timeout/package.json b/examples/typescript/daytona/process/run-daytona-with-timeout/package.json new file mode 100644 index 00000000..6f5c10bc --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-timeout/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-daytona-with-timeout", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/daytona": "file:../../../../../sdks/typescript/providers/daytona", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/daytona/process/run-daytona-with-timeout/src/main.ts b/examples/typescript/daytona/process/run-daytona-with-timeout/src/main.ts new file mode 100644 index 00000000..d0e4fef8 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-timeout/src/main.ts @@ -0,0 +1,19 @@ +import { daytona } from "@sandbox-kit/daytona"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: daytona(), + auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } }, + timeout: 120_000, + ...(process.env.DAYTONA_API_URL ? { endpoint: process.env.DAYTONA_API_URL } : {}), + ...(process.env.DAYTONA_TARGET ? { region: process.env.DAYTONA_TARGET } : {}), +}); + +const sandbox = await client.get(process.env.DAYTONA_SANDBOX_ID!); +const result = await sandbox.process.run({ + argv: ["echo", "hello with timeout"], + timeout: 5_000, +}); +console.log( + `exit=${result.exitCode} stdout=${JSON.stringify(new TextDecoder().decode(result.stdout))} stderr=${JSON.stringify(new TextDecoder().decode(result.stderr))}`, +); diff --git a/examples/typescript/daytona/process/run-daytona-with-timeout/tsconfig.json b/examples/typescript/daytona/process/run-daytona-with-timeout/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/daytona/process/run-daytona-with-timeout/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/examples/typescript/modal/process/run-modal-check-exit/.env.example b/examples/typescript/modal/process/run-modal-check-exit/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-check-exit/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/typescript/modal/process/run-modal-check-exit/.gitignore b/examples/typescript/modal/process/run-modal-check-exit/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-check-exit/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/modal/process/run-modal-check-exit/README.md b/examples/typescript/modal/process/run-modal-check-exit/README.md new file mode 100644 index 00000000..59f92f86 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-check-exit/README.md @@ -0,0 +1,48 @@ +# Check a non-zero Modal exit code + +Run `exit 7` via `shell` and read `exit_code` from the result. The program +loads that sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Modal returns both stdout and stderr. + +## Edge cases + +- A non-zero exit code is not an SDK error. Check `exit_code` / `exitCode` in + your program. +- Transport, validation, and Modal API failures still return errors. + +## Run + +```sh +cd examples/typescript/modal/process/run-modal-check-exit +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/modal/process/run-modal-check-exit/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/modal/process/run-modal-check-exit/package-lock.json b/examples/typescript/modal/process/run-modal-check-exit/package-lock.json new file mode 100644 index 00000000..a0c06afc --- /dev/null +++ b/examples/typescript/modal/process/run-modal-check-exit/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-modal-check-exit", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-modal-check-exit", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/modal": { + "name": "@sandbox-kit/modal", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@sandbox-kit/sandbox": "0.1.6", + "modal": "0.11.1" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/modal": { + "resolved": "../../../../../sdks/typescript/providers/modal", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/modal/process/run-modal-check-exit/package.json b/examples/typescript/modal/process/run-modal-check-exit/package.json new file mode 100644 index 00000000..83d474fa --- /dev/null +++ b/examples/typescript/modal/process/run-modal-check-exit/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-modal-check-exit", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/modal/process/run-modal-check-exit/src/main.ts b/examples/typescript/modal/process/run-modal-check-exit/src/main.ts new file mode 100644 index 00000000..51f52a7d --- /dev/null +++ b/examples/typescript/modal/process/run-modal-check-exit/src/main.ts @@ -0,0 +1,26 @@ +import { modal } from "@sandbox-kit/modal"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: modal(), + auth: { + tokenPair: { + id: process.env.MODAL_TOKEN_ID!, + secret: process.env.MODAL_TOKEN_SECRET!, + }, + }, + scope: { + appName: process.env.MODAL_APP_NAME!, + environment: process.env.MODAL_ENVIRONMENT || "main", + }, + timeout: 120_000, +}); + +const sandbox = await client.attach(process.env.MODAL_SANDBOX_ID!); +const result = await sandbox.process.run({ + shell: "exit 7", +}); +if (result.exitCode !== 7) { + throw new Error(`want exit 7, got ${result.exitCode}`); +} +console.log(`command finished with exit=${result.exitCode}`); diff --git a/examples/typescript/modal/process/run-modal-check-exit/tsconfig.json b/examples/typescript/modal/process/run-modal-check-exit/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-check-exit/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/examples/typescript/modal/process/run-modal-command/.env.example b/examples/typescript/modal/process/run-modal-command/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-command/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/typescript/modal/process/run-modal-command/.gitignore b/examples/typescript/modal/process/run-modal-command/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-command/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/modal/process/run-modal-command/README.md b/examples/typescript/modal/process/run-modal-command/README.md new file mode 100644 index 00000000..df839cd3 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-command/README.md @@ -0,0 +1,55 @@ +# Run a Modal command + +Run `echo hello from kit` with `sandbox.process.run` using `argv`. The program +opens that sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exitCode`, `stdout`, and +`stderr`. A non-zero exit code is not an SDK error. Modal returns both stdout and stderr. + +## Edge cases + +- Set exactly one of `argv` or `shell`. Setting both, or neither, fails before + Modal is called. The error field is `argv`. +- A relative working directory is rejected before Modal is called. The error + field is `working_directory`. +- Omit or zero `timeout` for no Modal command limit (the client omits + `timeoutMs`; sending `0` is rejected by Modal). Command timeouts must be + whole seconds (`2000` is valid; `1500` is rejected). The error field is + `timeout`. Aborting the signal rejects the await; the remote command may + keep running. + +## Run + +```sh +cd examples/typescript/modal/process/run-modal-command +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already +exists. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [go twin](../../../../go/modal/process/run-modal-command/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/modal/process/run-modal-command/package-lock.json b/examples/typescript/modal/process/run-modal-command/package-lock.json new file mode 100644 index 00000000..2b3163e5 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-command/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-modal-command", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-modal-command", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/modal": { + "name": "@sandbox-kit/modal", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@sandbox-kit/sandbox": "0.1.6", + "modal": "0.11.1" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/modal": { + "resolved": "../../../../../sdks/typescript/providers/modal", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/modal/process/run-modal-command/package.json b/examples/typescript/modal/process/run-modal-command/package.json new file mode 100644 index 00000000..05167725 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-command/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-modal-command", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/modal/process/run-modal-command/src/main.ts b/examples/typescript/modal/process/run-modal-command/src/main.ts new file mode 100644 index 00000000..b430cfc5 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-command/src/main.ts @@ -0,0 +1,25 @@ +import { modal } from "@sandbox-kit/modal"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: modal(), + auth: { + tokenPair: { + id: process.env.MODAL_TOKEN_ID!, + secret: process.env.MODAL_TOKEN_SECRET!, + }, + }, + scope: { + appName: process.env.MODAL_APP_NAME!, + environment: process.env.MODAL_ENVIRONMENT || "main", + }, + timeout: 120_000, +}); + +const sandbox = await client.attach(process.env.MODAL_SANDBOX_ID!); +const result = await sandbox.process.run({ + argv: ["echo", "hello from kit"], +}); +console.log( + `exit=${result.exitCode} stdout=${JSON.stringify(new TextDecoder().decode(result.stdout))} stderr=${JSON.stringify(new TextDecoder().decode(result.stderr))}`, +); diff --git a/examples/typescript/modal/process/run-modal-command/tsconfig.json b/examples/typescript/modal/process/run-modal-command/tsconfig.json new file mode 100644 index 00000000..b1e6e13e --- /dev/null +++ b/examples/typescript/modal/process/run-modal-command/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM", "ESNext.Disposable"], + "types": ["node"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/examples/typescript/modal/process/run-modal-shell-command/.env.example b/examples/typescript/modal/process/run-modal-shell-command/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-shell-command/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/typescript/modal/process/run-modal-shell-command/.gitignore b/examples/typescript/modal/process/run-modal-shell-command/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-shell-command/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/modal/process/run-modal-shell-command/README.md b/examples/typescript/modal/process/run-modal-shell-command/README.md new file mode 100644 index 00000000..e97990b5 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-shell-command/README.md @@ -0,0 +1,51 @@ +# Run a Modal shell command + +Run one shell string with `Process.Run` / `sandbox.process.run` using +`shell`. The program loads that sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Modal returns both stdout and stderr. + +## Edge cases + +- Set exactly one of `argv` or `shell`. Setting both, or neither, fails before + Modal is called. The error field is `argv`. +- A blank `shell` fails with field `shell`. `shell` runs as `bash -c`. +- Omit or zero `timeout` for no Modal command limit (the client omits + `timeoutMs`; sending `0` is rejected by Modal). Command timeouts must be + whole seconds. + +## Run + +```sh +cd examples/typescript/modal/process/run-modal-shell-command +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/modal/process/run-modal-shell-command/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/modal/process/run-modal-shell-command/package-lock.json b/examples/typescript/modal/process/run-modal-shell-command/package-lock.json new file mode 100644 index 00000000..e71701ab --- /dev/null +++ b/examples/typescript/modal/process/run-modal-shell-command/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-modal-shell-command", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-modal-shell-command", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/modal": { + "name": "@sandbox-kit/modal", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@sandbox-kit/sandbox": "0.1.6", + "modal": "0.11.1" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/modal": { + "resolved": "../../../../../sdks/typescript/providers/modal", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/modal/process/run-modal-shell-command/package.json b/examples/typescript/modal/process/run-modal-shell-command/package.json new file mode 100644 index 00000000..93ea84b4 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-shell-command/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-modal-shell-command", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/modal/process/run-modal-shell-command/src/main.ts b/examples/typescript/modal/process/run-modal-shell-command/src/main.ts new file mode 100644 index 00000000..d8be2f8b --- /dev/null +++ b/examples/typescript/modal/process/run-modal-shell-command/src/main.ts @@ -0,0 +1,23 @@ +import { modal } from "@sandbox-kit/modal"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: modal(), + auth: { + tokenPair: { + id: process.env.MODAL_TOKEN_ID!, + secret: process.env.MODAL_TOKEN_SECRET!, + }, + }, + scope: { + appName: process.env.MODAL_APP_NAME!, + environment: process.env.MODAL_ENVIRONMENT || "main", + }, + timeout: 120_000, +}); + +const sandbox = await client.attach(process.env.MODAL_SANDBOX_ID!); +const result = await sandbox.process.run({ + shell: 'echo "hello from shell"', +}); +console.log(`exit=${result.exitCode} stdout=${JSON.stringify(new TextDecoder().decode(result.stdout))}`); diff --git a/examples/typescript/modal/process/run-modal-shell-command/tsconfig.json b/examples/typescript/modal/process/run-modal-shell-command/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-shell-command/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/examples/typescript/modal/process/run-modal-with-cwd/.env.example b/examples/typescript/modal/process/run-modal-with-cwd/.env.example new file mode 100644 index 00000000..db4cbc20 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-cwd/.env.example @@ -0,0 +1,8 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +# Absolute working directory for this command. +MODAL_WORKING_DIRECTORY=/tmp diff --git a/examples/typescript/modal/process/run-modal-with-cwd/.gitignore b/examples/typescript/modal/process/run-modal-with-cwd/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-cwd/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/modal/process/run-modal-with-cwd/README.md b/examples/typescript/modal/process/run-modal-with-cwd/README.md new file mode 100644 index 00000000..10b8769a --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-cwd/README.md @@ -0,0 +1,51 @@ +# Run a Modal command with a working directory + +Run `pwd` with an absolute `working_directory`. The program loads that +sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Modal returns both stdout and stderr. + +## Edge cases + +- A relative working directory is rejected before Modal is called. The error + field is `working_directory`. +- Omit `working_directory` to use the Modal sandbox default. + +## Run + +```sh +cd examples/typescript/modal/process/run-modal-with-cwd +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +# Absolute working directory for this command. +MODAL_WORKING_DIRECTORY=/tmp + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/modal/process/run-modal-with-cwd/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/modal/process/run-modal-with-cwd/package-lock.json b/examples/typescript/modal/process/run-modal-with-cwd/package-lock.json new file mode 100644 index 00000000..fcae5937 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-cwd/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-modal-with-cwd", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-modal-with-cwd", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/modal": { + "name": "@sandbox-kit/modal", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@sandbox-kit/sandbox": "0.1.6", + "modal": "0.11.1" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/modal": { + "resolved": "../../../../../sdks/typescript/providers/modal", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/modal/process/run-modal-with-cwd/package.json b/examples/typescript/modal/process/run-modal-with-cwd/package.json new file mode 100644 index 00000000..a245fe4e --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-cwd/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-modal-with-cwd", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/modal/process/run-modal-with-cwd/src/main.ts b/examples/typescript/modal/process/run-modal-with-cwd/src/main.ts new file mode 100644 index 00000000..360ab37f --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-cwd/src/main.ts @@ -0,0 +1,25 @@ +import { modal } from "@sandbox-kit/modal"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: modal(), + auth: { + tokenPair: { + id: process.env.MODAL_TOKEN_ID!, + secret: process.env.MODAL_TOKEN_SECRET!, + }, + }, + scope: { + appName: process.env.MODAL_APP_NAME!, + environment: process.env.MODAL_ENVIRONMENT || "main", + }, + timeout: 120_000, +}); + +const sandbox = await client.attach(process.env.MODAL_SANDBOX_ID!); +const cwd = process.env.MODAL_WORKING_DIRECTORY || "/tmp"; +const result = await sandbox.process.run({ + argv: ["pwd"], + workingDirectory: cwd, +}); +console.log(`exit=${result.exitCode} cwd_out=${JSON.stringify(new TextDecoder().decode(result.stdout))}`); diff --git a/examples/typescript/modal/process/run-modal-with-cwd/tsconfig.json b/examples/typescript/modal/process/run-modal-with-cwd/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-cwd/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/examples/typescript/modal/process/run-modal-with-env/.env.example b/examples/typescript/modal/process/run-modal-with-env/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-env/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/typescript/modal/process/run-modal-with-env/.gitignore b/examples/typescript/modal/process/run-modal-with-env/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-env/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/modal/process/run-modal-with-env/README.md b/examples/typescript/modal/process/run-modal-with-env/README.md new file mode 100644 index 00000000..e030e9cc --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-env/README.md @@ -0,0 +1,48 @@ +# Run a Modal command with environment variables + +Run `printenv KIT_DEMO` with environment variables for that call only. The +program loads that sandbox with `client.attach`. + +## What this means + +The call waits for the command to finish and returns `exit_code`, `stdout`, +and `stderr`. A non-zero exit code is not an SDK error. Modal returns both stdout and stderr. + +## Edge cases + +- An omitted `environment` map leaves the Modal default. An empty map adds + no variables for this call. +- These variables apply to this command, not to later calls on the same handle. + +## Run + +```sh +cd examples/typescript/modal/process/run-modal-with-env +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/modal/process/run-modal-with-env/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/modal/process/run-modal-with-env/package-lock.json b/examples/typescript/modal/process/run-modal-with-env/package-lock.json new file mode 100644 index 00000000..302e5d15 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-env/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-modal-with-env", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-modal-with-env", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/modal": { + "name": "@sandbox-kit/modal", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@sandbox-kit/sandbox": "0.1.6", + "modal": "0.11.1" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/modal": { + "resolved": "../../../../../sdks/typescript/providers/modal", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/modal/process/run-modal-with-env/package.json b/examples/typescript/modal/process/run-modal-with-env/package.json new file mode 100644 index 00000000..8c659153 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-env/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-modal-with-env", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/modal/process/run-modal-with-env/src/main.ts b/examples/typescript/modal/process/run-modal-with-env/src/main.ts new file mode 100644 index 00000000..ba8d1567 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-env/src/main.ts @@ -0,0 +1,24 @@ +import { modal } from "@sandbox-kit/modal"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: modal(), + auth: { + tokenPair: { + id: process.env.MODAL_TOKEN_ID!, + secret: process.env.MODAL_TOKEN_SECRET!, + }, + }, + scope: { + appName: process.env.MODAL_APP_NAME!, + environment: process.env.MODAL_ENVIRONMENT || "main", + }, + timeout: 120_000, +}); + +const sandbox = await client.attach(process.env.MODAL_SANDBOX_ID!); +const result = await sandbox.process.run({ + argv: ["printenv", "KIT_DEMO"], + environment: { KIT_DEMO: "from-kit" } +}); +console.log(`exit=${result.exitCode} stdout=${JSON.stringify(new TextDecoder().decode(result.stdout))}`); diff --git a/examples/typescript/modal/process/run-modal-with-env/tsconfig.json b/examples/typescript/modal/process/run-modal-with-env/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-env/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/examples/typescript/modal/process/run-modal-with-timeout/.env.example b/examples/typescript/modal/process/run-modal-with-timeout/.env.example new file mode 100644 index 00000000..036b2df6 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-timeout/.env.example @@ -0,0 +1,5 @@ +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= diff --git a/examples/typescript/modal/process/run-modal-with-timeout/.gitignore b/examples/typescript/modal/process/run-modal-with-timeout/.gitignore new file mode 100644 index 00000000..a8b14857 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-timeout/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/examples/typescript/modal/process/run-modal-with-timeout/README.md b/examples/typescript/modal/process/run-modal-with-timeout/README.md new file mode 100644 index 00000000..0241ba4b --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-timeout/README.md @@ -0,0 +1,51 @@ +# Cap a Modal Process.Run timeout + +Run `echo hello with timeout` with `sandbox.process.run` / `Process.Run` and a +whole-second `timeout` of five seconds (`5000` ms). The program loads that +sandbox with `client.attach`. + +## What this means + +A positive `timeout` caps this call. Values must be whole seconds +(`5000` ms / `5 * time.Second`). Omit or zero adds no Modal command limit +(TypeScript omits `timeoutMs`; Go sends `Timeout: 0`). + +## Edge cases + +- `1500` ms is rejected before Modal runs. The error field is `timeout`. +- Abort the call (or cancel the Go context) to end the wait. The remote + command may keep running; the handle stays busy until that provider call + settles. + +## Run + +```sh +cd examples/typescript/modal/process/run-modal-with-timeout +cp .env.example .env +# Fill in the values below, then: +npm install && npm run build && npm start +``` + +Requires Node.js 20/22/24.15+ and npm 11.19.0. Skip copying if `.env` already exists. + +### Environment + +```dotenv +MODAL_TOKEN_ID= +MODAL_TOKEN_SECRET= +MODAL_APP_NAME= +MODAL_ENVIRONMENT=main +MODAL_SANDBOX_ID= + +``` + +## Warnings + +- Needs an existing sandbox. +- Closing the handle leaves the sandbox as it is. Closing the client closes + your connection to the provider. + +## Related + +- [Go twin](../../../../go/modal/process/run-modal-with-timeout/README.md) +- Catalog: [examples/typescript/README.md](../../../README.md) diff --git a/examples/typescript/modal/process/run-modal-with-timeout/package-lock.json b/examples/typescript/modal/process/run-modal-with-timeout/package-lock.json new file mode 100644 index 00000000..b312866f --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-timeout/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "sandbox-kit-example-run-modal-with-timeout", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandbox-kit-example-run-modal-with-timeout", + "version": "0.1.0", + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/providers/modal": { + "name": "@sandbox-kit/modal", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@sandbox-kit/sandbox": "0.1.6", + "modal": "0.11.1" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "../../../../../sdks/typescript/sandbox": { + "name": "@sandbox-kit/sandbox", + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protovalidate": "1.3.0", + "@sandbox-kit/contracts": "0.1.6" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3", + "vitest": "4.1.11" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + } + }, + "node_modules/@sandbox-kit/modal": { + "resolved": "../../../../../sdks/typescript/providers/modal", + "link": true + }, + "node_modules/@sandbox-kit/sandbox": { + "resolved": "../../../../../sdks/typescript/sandbox", + "link": true + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/typescript/modal/process/run-modal-with-timeout/package.json b/examples/typescript/modal/process/run-modal-with-timeout/package.json new file mode 100644 index 00000000..1fcffdd0 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-timeout/package.json @@ -0,0 +1,22 @@ +{ + "name": "sandbox-kit-example-run-modal-with-timeout", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.19.0", + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.15.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node --env-file-if-exists=.env dist/main.js" + }, + "dependencies": { + "@sandbox-kit/modal": "file:../../../../../sdks/typescript/providers/modal", + "@sandbox-kit/sandbox": "file:../../../../../sdks/typescript/sandbox" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "5.9.3" + } +} diff --git a/examples/typescript/modal/process/run-modal-with-timeout/src/main.ts b/examples/typescript/modal/process/run-modal-with-timeout/src/main.ts new file mode 100644 index 00000000..709ae9dd --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-timeout/src/main.ts @@ -0,0 +1,26 @@ +import { modal } from "@sandbox-kit/modal"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: modal(), + auth: { + tokenPair: { + id: process.env.MODAL_TOKEN_ID!, + secret: process.env.MODAL_TOKEN_SECRET!, + }, + }, + scope: { + appName: process.env.MODAL_APP_NAME!, + environment: process.env.MODAL_ENVIRONMENT || "main", + }, + timeout: 120_000, +}); + +const sandbox = await client.attach(process.env.MODAL_SANDBOX_ID!); +const result = await sandbox.process.run({ + argv: ["echo", "hello with timeout"], + timeout: 5_000, +}); +console.log( + `exit=${result.exitCode} stdout=${JSON.stringify(new TextDecoder().decode(result.stdout))} stderr=${JSON.stringify(new TextDecoder().decode(result.stderr))}`, +); diff --git a/examples/typescript/modal/process/run-modal-with-timeout/tsconfig.json b/examples/typescript/modal/process/run-modal-with-timeout/tsconfig.json new file mode 100644 index 00000000..6a4434f1 --- /dev/null +++ b/examples/typescript/modal/process/run-modal-with-timeout/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM", + "ESNext.Disposable" + ], + "types": [ + "node" + ], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/proto/kit/sandbox/v1/sandbox.proto b/proto/kit/sandbox/v1/sandbox.proto index ef84a2fc..a3e65125 100644 --- a/proto/kit/sandbox/v1/sandbox.proto +++ b/proto/kit/sandbox/v1/sandbox.proto @@ -561,6 +561,32 @@ message RemoveOptions { google.protobuf.Duration timeout = 1; optional bool recursive = 2; } + +// ExecOptions configures Process.Run. Set exactly one of argv or shell. +// Kit validates that exclusivity before the provider call. +// Nil timeout inherits Config.Timeout as the Kit deadline. Explicit zero adds +// no Kit deadline. Both omit and zero mean no provider command limit; a +// provider may send an explicit zero when its omitted native default would +// impose one. Provider command timeouts must be whole seconds. +message ExecOptions { + // Program and arguments. Mutually exclusive with shell. Must be non-empty when used. + repeated string argv = 1; + // Shell command string. Mutually exclusive with argv. + optional string shell = 2; + // Absolute working directory inside the sandbox. + optional string working_directory = 3; + // Environment variables for the command. An empty map is explicit; nil means omitted. + map environment = 4; + google.protobuf.Duration timeout = 5; +} + +// ExecResult is the outcome of Process.Run. A non-zero exit_code is not an error. +message ExecResult { + int32 exit_code = 1; + bytes stdout = 2; + bytes stderr = 3; +} + // SandboxState is a portable lifecycle state. ProviderState retains the native value. enum SandboxState { SANDBOX_STATE_UNKNOWN = 0; diff --git a/sdks/go/internal/contracts/kit/sandbox/v1/sandbox.pb.go b/sdks/go/internal/contracts/kit/sandbox/v1/sandbox.pb.go index 0aced011..ce2cdd20 100644 --- a/sdks/go/internal/contracts/kit/sandbox/v1/sandbox.pb.go +++ b/sdks/go/internal/contracts/kit/sandbox/v1/sandbox.pb.go @@ -3071,6 +3071,153 @@ func (x *RemoveOptions) GetRecursive() bool { return false } +// ExecOptions configures Process.Run. Set exactly one of argv or shell. +// Kit validates that exclusivity before the provider call. +// Nil timeout inherits Config.Timeout as the Kit deadline. Explicit zero adds +// no Kit deadline. Both omit and zero mean no provider command limit; a +// provider may send an explicit zero when its omitted native default would +// impose one. Provider command timeouts must be whole seconds. +type ExecOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Program and arguments. Mutually exclusive with shell. Must be non-empty when used. + Argv []string `protobuf:"bytes,1,rep,name=argv,proto3" json:"argv,omitempty"` + // Shell command string. Mutually exclusive with argv. + Shell *string `protobuf:"bytes,2,opt,name=shell,proto3,oneof" json:"shell,omitempty"` + // Absolute working directory inside the sandbox. + WorkingDirectory *string `protobuf:"bytes,3,opt,name=working_directory,json=workingDirectory,proto3,oneof" json:"working_directory,omitempty"` + // Environment variables for the command. An empty map is explicit; nil means omitted. + Environment map[string]string `protobuf:"bytes,4,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Timeout *durationpb.Duration `protobuf:"bytes,5,opt,name=timeout,proto3" json:"timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecOptions) Reset() { + *x = ExecOptions{} + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecOptions) ProtoMessage() {} + +func (x *ExecOptions) ProtoReflect() protoreflect.Message { + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecOptions.ProtoReflect.Descriptor instead. +func (*ExecOptions) Descriptor() ([]byte, []int) { + return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{37} +} + +func (x *ExecOptions) GetArgv() []string { + if x != nil { + return x.Argv + } + return nil +} + +func (x *ExecOptions) GetShell() string { + if x != nil && x.Shell != nil { + return *x.Shell + } + return "" +} + +func (x *ExecOptions) GetWorkingDirectory() string { + if x != nil && x.WorkingDirectory != nil { + return *x.WorkingDirectory + } + return "" +} + +func (x *ExecOptions) GetEnvironment() map[string]string { + if x != nil { + return x.Environment + } + return nil +} + +func (x *ExecOptions) GetTimeout() *durationpb.Duration { + if x != nil { + return x.Timeout + } + return nil +} + +// ExecResult is the outcome of Process.Run. A non-zero exit_code is not an error. +type ExecResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + ExitCode int32 `protobuf:"varint,1,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + Stdout []byte `protobuf:"bytes,2,opt,name=stdout,proto3" json:"stdout,omitempty"` + Stderr []byte `protobuf:"bytes,3,opt,name=stderr,proto3" json:"stderr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecResult) Reset() { + *x = ExecResult{} + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecResult) ProtoMessage() {} + +func (x *ExecResult) ProtoReflect() protoreflect.Message { + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecResult.ProtoReflect.Descriptor instead. +func (*ExecResult) Descriptor() ([]byte, []int) { + return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{38} +} + +func (x *ExecResult) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +func (x *ExecResult) GetStdout() []byte { + if x != nil { + return x.Stdout + } + return nil +} + +func (x *ExecResult) GetStderr() []byte { + if x != nil { + return x.Stderr + } + return nil +} + // SandboxInfo is shared metadata. Optional fields stay absent when the provider did not report them. // // Example: @@ -3103,7 +3250,7 @@ type SandboxInfo struct { func (x *SandboxInfo) Reset() { *x = SandboxInfo{} - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[37] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3115,7 +3262,7 @@ func (x *SandboxInfo) String() string { func (*SandboxInfo) ProtoMessage() {} func (x *SandboxInfo) ProtoReflect() protoreflect.Message { - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[37] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3128,7 +3275,7 @@ func (x *SandboxInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxInfo.ProtoReflect.Descriptor instead. func (*SandboxInfo) Descriptor() ([]byte, []int) { - return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{37} + return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{39} } func (x *SandboxInfo) GetId() string { @@ -3251,7 +3398,7 @@ type ErrorInfo struct { func (x *ErrorInfo) Reset() { *x = ErrorInfo{} - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[38] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3263,7 +3410,7 @@ func (x *ErrorInfo) String() string { func (*ErrorInfo) ProtoMessage() {} func (x *ErrorInfo) ProtoReflect() protoreflect.Message { - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[38] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3276,7 +3423,7 @@ func (x *ErrorInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ErrorInfo.ProtoReflect.Descriptor instead. func (*ErrorInfo) Descriptor() ([]byte, []int) { - return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{38} + return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{40} } func (x *ErrorInfo) GetKind() ErrorKind { @@ -3363,7 +3510,7 @@ type MetadataValue struct { func (x *MetadataValue) Reset() { *x = MetadataValue{} - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[39] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3375,7 +3522,7 @@ func (x *MetadataValue) String() string { func (*MetadataValue) ProtoMessage() {} func (x *MetadataValue) ProtoReflect() protoreflect.Message { - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[39] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3388,7 +3535,7 @@ func (x *MetadataValue) ProtoReflect() protoreflect.Message { // Deprecated: Use MetadataValue.ProtoReflect.Descriptor instead. func (*MetadataValue) Descriptor() ([]byte, []int) { - return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{39} + return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{41} } func (x *MetadataValue) GetValue() isMetadataValue_Value { @@ -3533,7 +3680,7 @@ type MetadataList struct { func (x *MetadataList) Reset() { *x = MetadataList{} - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[40] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3545,7 +3692,7 @@ func (x *MetadataList) String() string { func (*MetadataList) ProtoMessage() {} func (x *MetadataList) ProtoReflect() protoreflect.Message { - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[40] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3558,7 +3705,7 @@ func (x *MetadataList) ProtoReflect() protoreflect.Message { // Deprecated: Use MetadataList.ProtoReflect.Descriptor instead. func (*MetadataList) Descriptor() ([]byte, []int) { - return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{40} + return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{42} } func (x *MetadataList) GetValues() []*MetadataValue { @@ -3579,7 +3726,7 @@ type MetadataObject struct { func (x *MetadataObject) Reset() { *x = MetadataObject{} - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[41] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3591,7 +3738,7 @@ func (x *MetadataObject) String() string { func (*MetadataObject) ProtoMessage() {} func (x *MetadataObject) ProtoReflect() protoreflect.Message { - mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[41] + mi := &file_kit_sandbox_v1_sandbox_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3604,7 +3751,7 @@ func (x *MetadataObject) ProtoReflect() protoreflect.Message { // Deprecated: Use MetadataObject.ProtoReflect.Descriptor instead. func (*MetadataObject) Descriptor() ([]byte, []int) { - return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{41} + return file_kit_sandbox_v1_sandbox_proto_rawDescGZIP(), []int{43} } func (x *MetadataObject) GetValues() map[string]*MetadataValue { @@ -3859,7 +4006,23 @@ const file_kit_sandbox_v1_sandbox_proto_rawDesc = "" + "\atimeout\x18\x01 \x01(\v2\x19.google.protobuf.DurationR\atimeout\x12!\n" + "\trecursive\x18\x02 \x01(\bH\x00R\trecursive\x88\x01\x01B\f\n" + "\n" + - "_recursive\"\xaf\x05\n" + + "_recursive\"\xd3\x02\n" + + "\vExecOptions\x12\x12\n" + + "\x04argv\x18\x01 \x03(\tR\x04argv\x12\x19\n" + + "\x05shell\x18\x02 \x01(\tH\x00R\x05shell\x88\x01\x01\x120\n" + + "\x11working_directory\x18\x03 \x01(\tH\x01R\x10workingDirectory\x88\x01\x01\x12N\n" + + "\venvironment\x18\x04 \x03(\v2,.kit.sandbox.v1.ExecOptions.EnvironmentEntryR\venvironment\x123\n" + + "\atimeout\x18\x05 \x01(\v2\x19.google.protobuf.DurationR\atimeout\x1a>\n" + + "\x10EnvironmentEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\b\n" + + "\x06_shellB\x14\n" + + "\x12_working_directory\"Y\n" + + "\n" + + "ExecResult\x12\x1b\n" + + "\texit_code\x18\x01 \x01(\x05R\bexitCode\x12\x16\n" + + "\x06stdout\x18\x02 \x01(\fR\x06stdout\x12\x16\n" + + "\x06stderr\x18\x03 \x01(\fR\x06stderr\"\xaf\x05\n" + "\vSandboxInfo\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1a\n" + "\bprovider\x18\x02 \x01(\tR\bprovider\x12\x17\n" + @@ -4018,7 +4181,7 @@ func file_kit_sandbox_v1_sandbox_proto_rawDescGZIP() []byte { } var file_kit_sandbox_v1_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 11) -var file_kit_sandbox_v1_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 46) +var file_kit_sandbox_v1_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 49) var file_kit_sandbox_v1_sandbox_proto_goTypes = []any{ (SnapshotKind)(0), // 0: kit.sandbox.v1.SnapshotKind (IsolationKind)(0), // 1: kit.sandbox.v1.IsolationKind @@ -4068,17 +4231,20 @@ var file_kit_sandbox_v1_sandbox_proto_goTypes = []any{ (*ListDirectoryOptions)(nil), // 45: kit.sandbox.v1.ListDirectoryOptions (*StatOptions)(nil), // 46: kit.sandbox.v1.StatOptions (*RemoveOptions)(nil), // 47: kit.sandbox.v1.RemoveOptions - (*SandboxInfo)(nil), // 48: kit.sandbox.v1.SandboxInfo - (*ErrorInfo)(nil), // 49: kit.sandbox.v1.ErrorInfo - (*MetadataValue)(nil), // 50: kit.sandbox.v1.MetadataValue - (*MetadataList)(nil), // 51: kit.sandbox.v1.MetadataList - (*MetadataObject)(nil), // 52: kit.sandbox.v1.MetadataObject - nil, // 53: kit.sandbox.v1.CreateOptions.EnvironmentEntry - nil, // 54: kit.sandbox.v1.CreateOptions.LabelsEntry - nil, // 55: kit.sandbox.v1.SandboxInfo.LabelsEntry - nil, // 56: kit.sandbox.v1.MetadataObject.ValuesEntry - (*durationpb.Duration)(nil), // 57: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 58: google.protobuf.Timestamp + (*ExecOptions)(nil), // 48: kit.sandbox.v1.ExecOptions + (*ExecResult)(nil), // 49: kit.sandbox.v1.ExecResult + (*SandboxInfo)(nil), // 50: kit.sandbox.v1.SandboxInfo + (*ErrorInfo)(nil), // 51: kit.sandbox.v1.ErrorInfo + (*MetadataValue)(nil), // 52: kit.sandbox.v1.MetadataValue + (*MetadataList)(nil), // 53: kit.sandbox.v1.MetadataList + (*MetadataObject)(nil), // 54: kit.sandbox.v1.MetadataObject + nil, // 55: kit.sandbox.v1.CreateOptions.EnvironmentEntry + nil, // 56: kit.sandbox.v1.CreateOptions.LabelsEntry + nil, // 57: kit.sandbox.v1.ExecOptions.EnvironmentEntry + nil, // 58: kit.sandbox.v1.SandboxInfo.LabelsEntry + nil, // 59: kit.sandbox.v1.MetadataObject.ValuesEntry + (*durationpb.Duration)(nil), // 60: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 61: google.protobuf.Timestamp } var file_kit_sandbox_v1_sandbox_proto_depIdxs = []int32{ 12, // 0: kit.sandbox.v1.CreateOptions.source:type_name -> kit.sandbox.v1.SandboxSource @@ -4086,8 +4252,8 @@ var file_kit_sandbox_v1_sandbox_proto_depIdxs = []int32{ 18, // 2: kit.sandbox.v1.CreateOptions.isolation:type_name -> kit.sandbox.v1.IsolationConfig 19, // 3: kit.sandbox.v1.CreateOptions.resources:type_name -> kit.sandbox.v1.Resources 20, // 4: kit.sandbox.v1.CreateOptions.placement:type_name -> kit.sandbox.v1.Placement - 53, // 5: kit.sandbox.v1.CreateOptions.environment:type_name -> kit.sandbox.v1.CreateOptions.EnvironmentEntry - 54, // 6: kit.sandbox.v1.CreateOptions.labels:type_name -> kit.sandbox.v1.CreateOptions.LabelsEntry + 55, // 5: kit.sandbox.v1.CreateOptions.environment:type_name -> kit.sandbox.v1.CreateOptions.EnvironmentEntry + 56, // 6: kit.sandbox.v1.CreateOptions.labels:type_name -> kit.sandbox.v1.CreateOptions.LabelsEntry 22, // 7: kit.sandbox.v1.CreateOptions.network:type_name -> kit.sandbox.v1.NetworkConfig 24, // 8: kit.sandbox.v1.CreateOptions.security:type_name -> kit.sandbox.v1.SecurityConfig 26, // 9: kit.sandbox.v1.CreateOptions.lifetime:type_name -> kit.sandbox.v1.LifetimePolicy @@ -4108,51 +4274,53 @@ var file_kit_sandbox_v1_sandbox_proto_depIdxs = []int32{ 3, // 24: kit.sandbox.v1.PortBinding.transport:type_name -> kit.sandbox.v1.PortTransport 25, // 25: kit.sandbox.v1.SecurityConfig.secrets:type_name -> kit.sandbox.v1.SecretReference 4, // 26: kit.sandbox.v1.SecretReference.injection:type_name -> kit.sandbox.v1.SecretInjection - 57, // 27: kit.sandbox.v1.LifetimePolicy.maximum_lifetime:type_name -> google.protobuf.Duration + 60, // 27: kit.sandbox.v1.LifetimePolicy.maximum_lifetime:type_name -> google.protobuf.Duration 27, // 28: kit.sandbox.v1.LifetimePolicy.idle_terminate:type_name -> kit.sandbox.v1.AutomaticAction 27, // 29: kit.sandbox.v1.LifetimePolicy.idle_stop:type_name -> kit.sandbox.v1.AutomaticAction 27, // 30: kit.sandbox.v1.LifetimePolicy.idle_pause:type_name -> kit.sandbox.v1.AutomaticAction 27, // 31: kit.sandbox.v1.LifetimePolicy.stopped_archive:type_name -> kit.sandbox.v1.AutomaticAction 27, // 32: kit.sandbox.v1.LifetimePolicy.stopped_delete:type_name -> kit.sandbox.v1.AutomaticAction 5, // 33: kit.sandbox.v1.AutomaticAction.mode:type_name -> kit.sandbox.v1.PolicyMode - 57, // 34: kit.sandbox.v1.AutomaticAction.after:type_name -> google.protobuf.Duration - 57, // 35: kit.sandbox.v1.ProvisioningOptions.timeout:type_name -> google.protobuf.Duration - 57, // 36: kit.sandbox.v1.ProvisioningOptions.queue_timeout:type_name -> google.protobuf.Duration + 60, // 34: kit.sandbox.v1.AutomaticAction.after:type_name -> google.protobuf.Duration + 60, // 35: kit.sandbox.v1.ProvisioningOptions.timeout:type_name -> google.protobuf.Duration + 60, // 36: kit.sandbox.v1.ProvisioningOptions.queue_timeout:type_name -> google.protobuf.Duration 6, // 37: kit.sandbox.v1.ProvisioningOptions.wait_for:type_name -> kit.sandbox.v1.WaitCondition 30, // 38: kit.sandbox.v1.ReadinessProbe.command:type_name -> kit.sandbox.v1.CommandProbe - 48, // 39: kit.sandbox.v1.CreateResult.sandbox:type_name -> kit.sandbox.v1.SandboxInfo - 57, // 40: kit.sandbox.v1.StartOptions.timeout:type_name -> google.protobuf.Duration - 57, // 41: kit.sandbox.v1.StopOptions.timeout:type_name -> google.protobuf.Duration - 57, // 42: kit.sandbox.v1.PauseOptions.timeout:type_name -> google.protobuf.Duration - 57, // 43: kit.sandbox.v1.TerminateOptions.timeout:type_name -> google.protobuf.Duration - 57, // 44: kit.sandbox.v1.ArchiveOptions.timeout:type_name -> google.protobuf.Duration - 57, // 45: kit.sandbox.v1.DeleteOptions.timeout:type_name -> google.protobuf.Duration + 50, // 39: kit.sandbox.v1.CreateResult.sandbox:type_name -> kit.sandbox.v1.SandboxInfo + 60, // 40: kit.sandbox.v1.StartOptions.timeout:type_name -> google.protobuf.Duration + 60, // 41: kit.sandbox.v1.StopOptions.timeout:type_name -> google.protobuf.Duration + 60, // 42: kit.sandbox.v1.PauseOptions.timeout:type_name -> google.protobuf.Duration + 60, // 43: kit.sandbox.v1.TerminateOptions.timeout:type_name -> google.protobuf.Duration + 60, // 44: kit.sandbox.v1.ArchiveOptions.timeout:type_name -> google.protobuf.Duration + 60, // 45: kit.sandbox.v1.DeleteOptions.timeout:type_name -> google.protobuf.Duration 7, // 46: kit.sandbox.v1.FileInfo.type:type_name -> kit.sandbox.v1.FileType - 58, // 47: kit.sandbox.v1.FileInfo.modified_at:type_name -> google.protobuf.Timestamp - 57, // 48: kit.sandbox.v1.FileReadOptions.timeout:type_name -> google.protobuf.Duration - 57, // 49: kit.sandbox.v1.FileWriteOptions.timeout:type_name -> google.protobuf.Duration - 57, // 50: kit.sandbox.v1.FileCopyOptions.timeout:type_name -> google.protobuf.Duration - 57, // 51: kit.sandbox.v1.MakeDirectoryOptions.timeout:type_name -> google.protobuf.Duration - 57, // 52: kit.sandbox.v1.ListDirectoryOptions.timeout:type_name -> google.protobuf.Duration - 57, // 53: kit.sandbox.v1.StatOptions.timeout:type_name -> google.protobuf.Duration - 57, // 54: kit.sandbox.v1.RemoveOptions.timeout:type_name -> google.protobuf.Duration - 18, // 55: kit.sandbox.v1.SandboxInfo.isolation:type_name -> kit.sandbox.v1.IsolationConfig - 19, // 56: kit.sandbox.v1.SandboxInfo.resources:type_name -> kit.sandbox.v1.Resources - 55, // 57: kit.sandbox.v1.SandboxInfo.labels:type_name -> kit.sandbox.v1.SandboxInfo.LabelsEntry - 58, // 58: kit.sandbox.v1.SandboxInfo.created_at:type_name -> google.protobuf.Timestamp - 52, // 59: kit.sandbox.v1.SandboxInfo.provider_metadata:type_name -> kit.sandbox.v1.MetadataObject - 8, // 60: kit.sandbox.v1.SandboxInfo.state:type_name -> kit.sandbox.v1.SandboxState - 10, // 61: kit.sandbox.v1.ErrorInfo.kind:type_name -> kit.sandbox.v1.ErrorKind - 51, // 62: kit.sandbox.v1.MetadataValue.list:type_name -> kit.sandbox.v1.MetadataList - 52, // 63: kit.sandbox.v1.MetadataValue.object:type_name -> kit.sandbox.v1.MetadataObject - 50, // 64: kit.sandbox.v1.MetadataList.values:type_name -> kit.sandbox.v1.MetadataValue - 56, // 65: kit.sandbox.v1.MetadataObject.values:type_name -> kit.sandbox.v1.MetadataObject.ValuesEntry - 50, // 66: kit.sandbox.v1.MetadataObject.ValuesEntry.value:type_name -> kit.sandbox.v1.MetadataValue - 67, // [67:67] is the sub-list for method output_type - 67, // [67:67] is the sub-list for method input_type - 67, // [67:67] is the sub-list for extension type_name - 67, // [67:67] is the sub-list for extension extendee - 0, // [0:67] is the sub-list for field type_name + 61, // 47: kit.sandbox.v1.FileInfo.modified_at:type_name -> google.protobuf.Timestamp + 60, // 48: kit.sandbox.v1.FileReadOptions.timeout:type_name -> google.protobuf.Duration + 60, // 49: kit.sandbox.v1.FileWriteOptions.timeout:type_name -> google.protobuf.Duration + 60, // 50: kit.sandbox.v1.FileCopyOptions.timeout:type_name -> google.protobuf.Duration + 60, // 51: kit.sandbox.v1.MakeDirectoryOptions.timeout:type_name -> google.protobuf.Duration + 60, // 52: kit.sandbox.v1.ListDirectoryOptions.timeout:type_name -> google.protobuf.Duration + 60, // 53: kit.sandbox.v1.StatOptions.timeout:type_name -> google.protobuf.Duration + 60, // 54: kit.sandbox.v1.RemoveOptions.timeout:type_name -> google.protobuf.Duration + 57, // 55: kit.sandbox.v1.ExecOptions.environment:type_name -> kit.sandbox.v1.ExecOptions.EnvironmentEntry + 60, // 56: kit.sandbox.v1.ExecOptions.timeout:type_name -> google.protobuf.Duration + 18, // 57: kit.sandbox.v1.SandboxInfo.isolation:type_name -> kit.sandbox.v1.IsolationConfig + 19, // 58: kit.sandbox.v1.SandboxInfo.resources:type_name -> kit.sandbox.v1.Resources + 58, // 59: kit.sandbox.v1.SandboxInfo.labels:type_name -> kit.sandbox.v1.SandboxInfo.LabelsEntry + 61, // 60: kit.sandbox.v1.SandboxInfo.created_at:type_name -> google.protobuf.Timestamp + 54, // 61: kit.sandbox.v1.SandboxInfo.provider_metadata:type_name -> kit.sandbox.v1.MetadataObject + 8, // 62: kit.sandbox.v1.SandboxInfo.state:type_name -> kit.sandbox.v1.SandboxState + 10, // 63: kit.sandbox.v1.ErrorInfo.kind:type_name -> kit.sandbox.v1.ErrorKind + 53, // 64: kit.sandbox.v1.MetadataValue.list:type_name -> kit.sandbox.v1.MetadataList + 54, // 65: kit.sandbox.v1.MetadataValue.object:type_name -> kit.sandbox.v1.MetadataObject + 52, // 66: kit.sandbox.v1.MetadataList.values:type_name -> kit.sandbox.v1.MetadataValue + 59, // 67: kit.sandbox.v1.MetadataObject.values:type_name -> kit.sandbox.v1.MetadataObject.ValuesEntry + 52, // 68: kit.sandbox.v1.MetadataObject.ValuesEntry.value:type_name -> kit.sandbox.v1.MetadataValue + 69, // [69:69] is the sub-list for method output_type + 69, // [69:69] is the sub-list for method input_type + 69, // [69:69] is the sub-list for extension type_name + 69, // [69:69] is the sub-list for extension extendee + 0, // [0:69] is the sub-list for field type_name } func init() { file_kit_sandbox_v1_sandbox_proto_init() } @@ -4189,8 +4357,9 @@ func file_kit_sandbox_v1_sandbox_proto_init() { file_kit_sandbox_v1_sandbox_proto_msgTypes[33].OneofWrappers = []any{} file_kit_sandbox_v1_sandbox_proto_msgTypes[36].OneofWrappers = []any{} file_kit_sandbox_v1_sandbox_proto_msgTypes[37].OneofWrappers = []any{} - file_kit_sandbox_v1_sandbox_proto_msgTypes[38].OneofWrappers = []any{} - file_kit_sandbox_v1_sandbox_proto_msgTypes[39].OneofWrappers = []any{ + file_kit_sandbox_v1_sandbox_proto_msgTypes[39].OneofWrappers = []any{} + file_kit_sandbox_v1_sandbox_proto_msgTypes[40].OneofWrappers = []any{} + file_kit_sandbox_v1_sandbox_proto_msgTypes[41].OneofWrappers = []any{ (*MetadataValue_Boolean)(nil), (*MetadataValue_Text)(nil), (*MetadataValue_SignedInteger)(nil), @@ -4206,7 +4375,7 @@ func file_kit_sandbox_v1_sandbox_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_kit_sandbox_v1_sandbox_proto_rawDesc), len(file_kit_sandbox_v1_sandbox_proto_rawDesc)), NumEnums: 11, - NumMessages: 46, + NumMessages: 49, NumExtensions: 0, NumServices: 0, }, diff --git a/sdks/go/internal/sdk/timeout.go b/sdks/go/internal/sdk/timeout.go new file mode 100644 index 00000000..69b50beb --- /dev/null +++ b/sdks/go/internal/sdk/timeout.go @@ -0,0 +1,21 @@ +package sdk + +import ( + "time" + + durationpb "google.golang.org/protobuf/types/known/durationpb" +) + +// WholeSecondDuration maps a command timeout for provider exec APIs. +// Nil and explicit zero become 0 (no command limit once the provider applies +// that value). Non-zero values must be whole seconds; ok is false otherwise. +func WholeSecondDuration(value *durationpb.Duration) (time.Duration, bool) { + timeout := OptionalDuration(value) + if timeout == nil { + return 0, true + } + if *timeout < 0 || *timeout%time.Second != 0 { + return 0, false + } + return *timeout, true +} diff --git a/sdks/go/internal/sdk/timeout_test.go b/sdks/go/internal/sdk/timeout_test.go new file mode 100644 index 00000000..2025b156 --- /dev/null +++ b/sdks/go/internal/sdk/timeout_test.go @@ -0,0 +1,23 @@ +package sdk + +import ( + "testing" + "time" + + durationpb "google.golang.org/protobuf/types/known/durationpb" +) + +func TestWholeSecondDuration(t *testing.T) { + if got, ok := WholeSecondDuration(nil); !ok || got != 0 { + t.Fatalf("nil = %v, %v", got, ok) + } + if got, ok := WholeSecondDuration(durationpb.New(0)); !ok || got != 0 { + t.Fatalf("zero = %v, %v", got, ok) + } + if got, ok := WholeSecondDuration(durationpb.New(5 * time.Second)); !ok || got != 5*time.Second { + t.Fatalf("whole = %v, %v", got, ok) + } + if _, ok := WholeSecondDuration(durationpb.New(1500 * time.Millisecond)); ok { + t.Fatal("fractional accepted") + } +} diff --git a/sdks/go/providers/daytona/README.md b/sdks/go/providers/daytona/README.md index 2400bae6..962576e8 100644 --- a/sdks/go/providers/daytona/README.md +++ b/sdks/go/providers/daytona/README.md @@ -85,19 +85,32 @@ exists. A missing file passed to `CopyFromLocal` is not found, and the error field is `local_path`. A local directory is an invalid argument with the same field. `CopyToLocal` creates a missing local parent directory. +`Process().Run` runs one command and waits for the result. Set exactly one of +`Argv` or `Shell`. `Argv` is sent as a POSIX-quoted shell string. A non-zero +exit code is success for the SDK—read `GetExitCode`. Daytona returns stdout +only; `stderr` is a non-nil empty slice. Omit or zero `Timeout` so the command +is not capped by Daytona's 10-second native default. Command timeouts must be +whole seconds. Cancel the context to stop waiting. + CPU is a whole number of cores. Memory and disk are a whole number of GiB, written as MiB. A resource override requires an image source. A snapshot create uses the snapshot's resources. ## Examples -| Operation | Program | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| Create | [create-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-sandbox) | -| Read a file | [read-daytona-file](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/read-daytona-file) | -| Policies | [create-daytona-with-policies](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-with-policies) | -| Look up | [get-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/lifecycle/get-daytona-sandbox) | -| All | [Go examples](https://github.com/sandbox-kit/kit/tree/main/examples/go) | +| Operation | Program | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | +| Create | [create-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-sandbox) | +| Run a command | [run-daytona-command](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process/run-daytona-command) | +| Shell / cwd / env / exit | [process examples](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process) | +| Read a file | [read-daytona-file](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/read-daytona-file) | +| Filesystem | [filesystem examples](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem) | +| Policies | [create-daytona-with-policies](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-with-policies) | +| Look up | [get-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/lifecycle/get-daytona-sandbox) | +| All | [Go examples](https://github.com/sandbox-kit/kit/tree/main/examples/go) | + +pkg.go.dev also shows `ExampleNew_*` samples in this package for create, +lifecycle, process, and filesystem. ## License diff --git a/sdks/go/providers/daytona/doc.go b/sdks/go/providers/daytona/doc.go index 19704e80..8e613d5a 100644 --- a/sdks/go/providers/daytona/doc.go +++ b/sdks/go/providers/daytona/doc.go @@ -2,6 +2,7 @@ // // Pass [New] to sandbox.NewClient. [sandbox.Client.Create] with a nil request // uses Daytona's default snapshot. The backend implements shared filesystem -// operations through [sandbox.SandboxFiles]. Closing the client releases the -// local SDK and leaves the Daytona sandbox running. +// operations through [sandbox.SandboxFiles] and command execution through +// [sandbox.SandboxProcess]. Closing the client releases the local SDK and +// leaves the Daytona sandbox running. package daytona diff --git a/sdks/go/providers/daytona/example_test.go b/sdks/go/providers/daytona/example_test.go index d790c535..fbfde299 100644 --- a/sdks/go/providers/daytona/example_test.go +++ b/sdks/go/providers/daytona/example_test.go @@ -242,3 +242,160 @@ func ExampleNew_errors() { } defer client.Close(context.Background()) } + +func ExampleNew_process() { + client, err := sandbox.NewClient(daytona.New(), &sandbox.Config{}) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Get(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, + }) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(result.GetExitCode(), string(result.GetStdout())) +} + +func ExampleNew_processShell() { + client, err := sandbox.NewClient(daytona.New(), &sandbox.Config{}) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Get(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Shell: sandbox.Value(`echo "hello from shell"`), + }) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(result.GetExitCode()) +} + +func ExampleNew_processEnv() { + client, err := sandbox.NewClient(daytona.New(), &sandbox.Config{}) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Get(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"printenv", "KIT_DEMO"}, + Environment: map[string]string{"KIT_DEMO": "from-kit"}, + }) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(result.GetStdout())) +} + +func ExampleNew_filesystem() { + client, err := sandbox.NewClient(daytona.New(), &sandbox.Config{}) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Get(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + files := instance.Filesystem() + if err := files.WriteFile(context.Background(), "/tmp/note.txt", []byte("hello\n"), nil); err != nil { + fmt.Println(err) + return + } + data, err := files.ReadFile(context.Background(), "/tmp/note.txt", nil) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(data)) +} + +func ExampleNew_makeDirectory() { + client, err := sandbox.NewClient(daytona.New(), &sandbox.Config{}) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Get(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + err = instance.Filesystem().MakeDirectory(context.Background(), "/tmp/kit-dir", nil) + if err != nil { + fmt.Println(err) + } +} + +func ExampleNew_listDirectory() { + client, err := sandbox.NewClient(daytona.New(), &sandbox.Config{}) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Get(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + entries, err := instance.Filesystem().ListDirectory(context.Background(), "/tmp", nil) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(len(entries)) +} + +func ExampleNew_stat() { + client, err := sandbox.NewClient(daytona.New(), &sandbox.Config{}) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Get(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + info, err := instance.Filesystem().Stat(context.Background(), "/tmp/note.txt", nil) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(info.GetPath(), info.GetSize()) +} diff --git a/sdks/go/providers/daytona/process.go b/sdks/go/providers/daytona/process.go new file mode 100644 index 00000000..bde801cb --- /dev/null +++ b/sdks/go/providers/daytona/process.go @@ -0,0 +1,120 @@ +package daytona + +import ( + "context" + "strings" + "time" + + daytona "github.com/daytona/clients/sdk-go/pkg/daytona" + daytonaopts "github.com/daytona/clients/sdk-go/pkg/options" + "github.com/sandbox-kit/kit/sdks/go/internal/sdk" + "github.com/sandbox-kit/kit/sdks/go/sandbox" + "google.golang.org/protobuf/types/known/durationpb" +) + +var _ sandbox.SandboxProcess = (*backend)(nil) + +func (b *backend) RunCommand(ctx context.Context, id string, request *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + instance, err := b.processSandbox(ctx, id, "run") + if err != nil { + return nil, err + } + command, err := daytonaCommand(request) + if err != nil { + return nil, err + } + timeout, err := commandTimeout("run", request.GetTimeout()) + if err != nil { + return nil, err + } + opts := []func(*daytonaopts.ExecuteCommand){ + daytonaopts.WithExecuteTimeout(timeout), + } + if request.WorkingDirectory != nil { + opts = append(opts, daytonaopts.WithCwd(*request.WorkingDirectory)) + } + if request.Environment != nil { + opts = append(opts, daytonaopts.WithCommandEnv(request.Environment)) + } + response, err := instance.Process.ExecuteCommand(ctx, command, opts...) + if err != nil { + return nil, mapProviderError(err, "run") + } + if response == nil { + return nil, sandbox.NewError(&sandbox.ErrorInfo{ + Kind: sandbox.ErrorKindInvalidResponse, + Provider: "daytona", + Operation: "run", + Message: "sandbox-kit daytona: SDK returned no exec result", + }, nil) + } + return &sandbox.ExecResult{ + ExitCode: int32(response.ExitCode), + Stdout: []byte(response.Result), + Stderr: []byte{}, + }, nil +} + +func (b *backend) processSandbox(ctx context.Context, id, operation string) (*daytona.Sandbox, error) { + instance, err := b.nativeSandbox(ctx, id) + if err != nil { + return nil, mapProviderError(err, operation) + } + if instance == nil || instance.Process == nil { + return nil, sandbox.NewError(&sandbox.ErrorInfo{ + Kind: sandbox.ErrorKindInvalidResponse, + Provider: "daytona", + Operation: operation, + Message: "sandbox-kit daytona: SDK returned no process API", + }, nil) + } + return instance, nil +} + +func daytonaCommand(request *sandbox.ExecOptions) (string, error) { + if request.Shell != nil { + return *request.Shell, nil + } + if len(request.Argv) == 0 { + return "", sandbox.NewError(&sandbox.ErrorInfo{ + Kind: sandbox.ErrorKindInvalidArgument, + Provider: "daytona", + Operation: "run", + Field: sandbox.ExecFieldArgv, + Message: "sandbox-kit: set argv or shell", + }, nil) + } + return posixShellJoin(request.Argv), nil +} + +// commandTimeout maps Kit ExecOptions.timeout to Daytona's command timeout. +// Nil and explicit zero become 0 so Daytona adds no command limit (its omitted +// default is 10 seconds). Non-zero values must be whole seconds. +func commandTimeout(operation string, value *durationpb.Duration) (time.Duration, error) { + timeout, ok := sdk.WholeSecondDuration(value) + if !ok { + return 0, sandbox.NewError(&sandbox.ErrorInfo{ + Kind: sandbox.ErrorKindInvalidArgument, + Provider: "daytona", + Operation: operation, + Field: sandbox.ExecFieldTimeout, + Message: "sandbox-kit daytona: timeout must be a whole number of seconds", + }, nil) + } + return timeout, nil +} + +func posixShellJoin(argv []string) string { + parts := make([]string, len(argv)) + for i, arg := range argv { + parts[i] = posixQuote(arg) + } + return strings.Join(parts, " ") +} + +func posixQuote(value string) string { + if value == "" { + return "''" + } + return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'" +} diff --git a/sdks/go/providers/daytona/process_test.go b/sdks/go/providers/daytona/process_test.go new file mode 100644 index 00000000..e0e7496f --- /dev/null +++ b/sdks/go/providers/daytona/process_test.go @@ -0,0 +1,34 @@ +package daytona + +import ( + "errors" + "testing" + "time" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func TestPosixShellJoin(t *testing.T) { + got := posixShellJoin([]string{"echo", "hello world", "it's"}) + want := `'echo' 'hello world' 'it'"'"'s'` + if got != want { + t.Fatalf("posixShellJoin = %q, want %q", got, want) + } +} + +func TestCommandTimeout(t *testing.T) { + if got, err := commandTimeout("run", nil); err != nil || got != 0 { + t.Fatalf("nil timeout = %v, %v", got, err) + } + if got, err := commandTimeout("run", sandbox.Duration(0)); err != nil || got != 0 { + t.Fatalf("zero timeout = %v, %v", got, err) + } + if got, err := commandTimeout("run", sandbox.Duration(5*time.Second)); err != nil || got != 5*time.Second { + t.Fatalf("whole timeout = %v, %v", got, err) + } + _, err := commandTimeout("run", sandbox.Duration(1500*time.Millisecond)) + var detail *sandbox.Error + if !errors.As(err, &detail) || detail.Field != sandbox.ExecFieldTimeout { + t.Fatalf("fractional timeout = %#v", err) + } +} diff --git a/sdks/go/providers/modal/README.md b/sdks/go/providers/modal/README.md index ef15f196..383bd3ad 100644 --- a/sdks/go/providers/modal/README.md +++ b/sdks/go/providers/modal/README.md @@ -87,19 +87,31 @@ to `CopyFromLocal` is not found, and the error field is `local_path`. A local directory is an invalid argument with the same field. `CopyToLocal` creates a missing local parent directory. +`Process().Run` runs one command and waits for the result. Set exactly one of +`Argv` or `Shell`. `Shell` runs as `bash -c`. A non-zero exit code is success +for the SDK—read `GetExitCode`. Modal returns both stdout and stderr. Omit or +zero `Timeout` for no Modal command limit. Command timeouts must be whole +seconds. Cancel the context to stop waiting. + When you set resources, Modal requires at least 0.125 CPU cores and 128 MiB of memory. Omitted values keep Modal's defaults. Disk size cannot be raised at create time. ## Examples -| Operation | Program | -| ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Create | [create-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/configuration/create-modal-sandbox) | -| Read a file | [read-modal-file](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/read-modal-file) | -| Resources | [create-modal-with-resources](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/configuration/create-modal-with-resources) | -| Attach | [attach-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/lifecycle/attach-modal-sandbox) | -| All | [Go examples](https://github.com/sandbox-kit/kit/tree/main/examples/go) | +| Operation | Program | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Create | [create-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/configuration/create-modal-sandbox) | +| Run a command | [run-modal-command](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process/run-modal-command) | +| Shell / cwd / env / exit | [process examples](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process) | +| Read a file | [read-modal-file](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/read-modal-file) | +| Filesystem | [filesystem examples](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem) | +| Resources | [create-modal-with-resources](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/configuration/create-modal-with-resources) | +| Attach | [attach-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/lifecycle/attach-modal-sandbox) | +| All | [Go examples](https://github.com/sandbox-kit/kit/tree/main/examples/go) | + +pkg.go.dev also shows `ExampleNew_*` samples in this package for create, +lifecycle, process, and filesystem. ## License diff --git a/sdks/go/providers/modal/doc.go b/sdks/go/providers/modal/doc.go index c42b3082..e800aac5 100644 --- a/sdks/go/providers/modal/doc.go +++ b/sdks/go/providers/modal/doc.go @@ -2,7 +2,8 @@ // // Pass [New] to sandbox.NewClient. Create requires an image. Set Scope.AppName // to an existing Modal app. The backend implements shared filesystem -// operations through [sandbox.SandboxFiles]. Closing the client releases the -// local SDK and leaves the Modal sandbox running. Reclaim it with Terminate or -// a lifetime policy. +// operations through [sandbox.SandboxFiles] and command execution through +// [sandbox.SandboxProcess]. Closing the client releases the local SDK and +// leaves the Modal sandbox running. Reclaim it with Terminate or a lifetime +// policy. package modal diff --git a/sdks/go/providers/modal/example_test.go b/sdks/go/providers/modal/example_test.go index be88738e..c6b9cea7 100644 --- a/sdks/go/providers/modal/example_test.go +++ b/sdks/go/providers/modal/example_test.go @@ -98,3 +98,174 @@ func ExampleNew_errors() { // Output: // endpoint } + +func ExampleNew_process() { + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Scope: &sandbox.Scope{AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME"))}, + }) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Attach(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, + }) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(result.GetExitCode(), string(result.GetStdout())) +} + +func ExampleNew_processShell() { + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Scope: &sandbox.Scope{AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME"))}, + }) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Attach(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Shell: sandbox.Value(`echo "hello from shell"`), + }) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(result.GetExitCode()) +} + +func ExampleNew_processEnv() { + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Scope: &sandbox.Scope{AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME"))}, + }) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Attach(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"printenv", "KIT_DEMO"}, + Environment: map[string]string{"KIT_DEMO": "from-kit"}, + }) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(result.GetStdout())) +} + +func ExampleNew_filesystem() { + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Scope: &sandbox.Scope{AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME"))}, + }) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Attach(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + files := instance.Filesystem() + if err := files.WriteFile(context.Background(), "/tmp/note.txt", []byte("hello\n"), nil); err != nil { + fmt.Println(err) + return + } + data, err := files.ReadFile(context.Background(), "/tmp/note.txt", nil) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(data)) +} + +func ExampleNew_makeDirectory() { + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Scope: &sandbox.Scope{AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME"))}, + }) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Attach(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + err = instance.Filesystem().MakeDirectory(context.Background(), "/tmp/kit-dir", nil) + if err != nil { + fmt.Println(err) + } +} + +func ExampleNew_listDirectory() { + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Scope: &sandbox.Scope{AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME"))}, + }) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Attach(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + entries, err := instance.Filesystem().ListDirectory(context.Background(), "/tmp", nil) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(len(entries)) +} + +func ExampleNew_stat() { + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Scope: &sandbox.Scope{AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME"))}, + }) + if err != nil { + fmt.Println(err) + return + } + defer client.Close(context.Background()) + + instance, err := client.Attach(context.Background(), os.Getenv("SANDBOX_ID")) + if err != nil { + fmt.Println(err) + return + } + info, err := instance.Filesystem().Stat(context.Background(), "/tmp/note.txt", nil) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(info.GetPath(), info.GetSize()) +} diff --git a/sdks/go/providers/modal/process.go b/sdks/go/providers/modal/process.go new file mode 100644 index 00000000..3d8042cd --- /dev/null +++ b/sdks/go/providers/modal/process.go @@ -0,0 +1,165 @@ +package modal + +import ( + "context" + "io" + "sync" + "time" + + modalsdk "github.com/modal-labs/modal-client/go" + internalsdk "github.com/sandbox-kit/kit/sdks/go/internal/sdk" + "github.com/sandbox-kit/kit/sdks/go/sandbox" + "google.golang.org/protobuf/types/known/durationpb" +) + +var _ sandbox.SandboxProcess = (*backend)(nil) + +func (b *backend) RunCommand(ctx context.Context, id string, request *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + instance, err := b.processSandbox(ctx, id, "run") + if err != nil { + return nil, err + } + command, err := modalCommand(request) + if err != nil { + return nil, err + } + timeout, err := commandTimeout("run", request.GetTimeout()) + if err != nil { + return nil, err + } + params := &modalsdk.SandboxExecParams{ + Stdout: modalsdk.Pipe, + Stderr: modalsdk.Pipe, + Timeout: timeout, + } + if request.WorkingDirectory != nil { + params.Workdir = *request.WorkingDirectory + } + if request.Environment != nil { + params.Env = request.Environment + } + process, err := instance.Exec(ctx, command, params) + if err != nil { + return nil, mapProviderError(err, "run") + } + if process == nil { + return nil, sandbox.NewError(&sandbox.ErrorInfo{ + Kind: sandbox.ErrorKindInvalidResponse, + Provider: "modal", + Operation: "run", + Message: "sandbox-kit modal: SDK returned no process", + }, nil) + } + if process.Stdin != nil { + _ = process.Stdin.Close() + } + stdout, stderr, exitCode, err := collectProcess(ctx, process) + if err != nil { + return nil, mapProviderError(err, "run") + } + return &sandbox.ExecResult{ + ExitCode: int32(exitCode), + Stdout: stdout, + Stderr: stderr, + }, nil +} + +func (b *backend) processSandbox(ctx context.Context, id, operation string) (*modalsdk.Sandbox, error) { + instance, err := b.client.Sandboxes.FromID(ctx, id, nil) + if err != nil { + return nil, mapProviderError(err, operation) + } + if instance == nil { + return nil, sandbox.NewError(&sandbox.ErrorInfo{ + Kind: sandbox.ErrorKindInvalidResponse, + Provider: "modal", + Operation: operation, + Message: "sandbox-kit modal: SDK returned no sandbox", + }, nil) + } + return instance, nil +} + +func modalCommand(request *sandbox.ExecOptions) ([]string, error) { + if request.Shell != nil { + return []string{"bash", "-c", *request.Shell}, nil + } + if len(request.Argv) == 0 { + return nil, sandbox.NewError(&sandbox.ErrorInfo{ + Kind: sandbox.ErrorKindInvalidArgument, + Provider: "modal", + Operation: "run", + Field: sandbox.ExecFieldArgv, + Message: "sandbox-kit: set argv or shell", + }, nil) + } + return append([]string(nil), request.Argv...), nil +} + +// commandTimeout maps Kit ExecOptions.timeout to Modal's command timeout. +// Nil and explicit zero become 0 (no command timeout). Non-zero values must +// be whole seconds; Modal rejects fractional durations. +func commandTimeout(operation string, value *durationpb.Duration) (time.Duration, error) { + timeout, ok := internalsdk.WholeSecondDuration(value) + if !ok { + return 0, sandbox.NewError(&sandbox.ErrorInfo{ + Kind: sandbox.ErrorKindInvalidArgument, + Provider: "modal", + Operation: operation, + Field: sandbox.ExecFieldTimeout, + Message: "sandbox-kit modal: timeout must be a whole number of seconds", + }, nil) + } + return timeout, nil +} + +func collectProcess(ctx context.Context, process *modalsdk.ContainerProcess) (stdout, stderr []byte, exitCode int, err error) { + var wg sync.WaitGroup + var stdoutErr, stderrErr error + wg.Add(2) + go func() { + defer wg.Done() + if process.Stdout == nil { + return + } + stdout, stdoutErr = io.ReadAll(process.Stdout) + }() + go func() { + defer wg.Done() + if process.Stderr == nil { + return + } + stderr, stderrErr = io.ReadAll(process.Stderr) + }() + exitCode, err = process.Wait(ctx, nil) + if err != nil { + // Modal wires exec stdio with context.Background(), so a canceled Wait + // alone leaves ReadAll blocked. Close unblocks those reads. + closeProcessStreams(process) + wg.Wait() + return nil, nil, 0, err + } + wg.Wait() + if stdoutErr != nil { + return nil, nil, 0, stdoutErr + } + if stderrErr != nil { + return nil, nil, 0, stderrErr + } + if stdout == nil { + stdout = []byte{} + } + if stderr == nil { + stderr = []byte{} + } + return stdout, stderr, exitCode, nil +} + +func closeProcessStreams(process *modalsdk.ContainerProcess) { + if process.Stdout != nil { + _ = process.Stdout.Close() + } + if process.Stderr != nil { + _ = process.Stderr.Close() + } +} diff --git a/sdks/go/providers/modal/process_test.go b/sdks/go/providers/modal/process_test.go new file mode 100644 index 00000000..7a173c90 --- /dev/null +++ b/sdks/go/providers/modal/process_test.go @@ -0,0 +1,37 @@ +package modal + +import ( + "errors" + "testing" + "time" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func TestModalCommand(t *testing.T) { + argv, err := modalCommand(&sandbox.ExecOptions{Argv: []string{"echo", "hi"}}) + if err != nil || len(argv) != 2 || argv[0] != "echo" || argv[1] != "hi" { + t.Fatalf("argv = %#v, %v", argv, err) + } + shell, err := modalCommand(&sandbox.ExecOptions{Shell: sandbox.Value("echo hi")}) + if err != nil || len(shell) != 3 || shell[0] != "bash" || shell[1] != "-c" || shell[2] != "echo hi" { + t.Fatalf("shell = %#v, %v", shell, err) + } +} + +func TestCommandTimeout(t *testing.T) { + if got, err := commandTimeout("run", nil); err != nil || got != 0 { + t.Fatalf("nil timeout = %v, %v", got, err) + } + if got, err := commandTimeout("run", sandbox.Duration(0)); err != nil || got != 0 { + t.Fatalf("zero timeout = %v, %v", got, err) + } + if got, err := commandTimeout("run", sandbox.Duration(5*time.Second)); err != nil || got != 5*time.Second { + t.Fatalf("whole timeout = %v, %v", got, err) + } + _, err := commandTimeout("run", sandbox.Duration(1500*time.Millisecond)) + var detail *sandbox.Error + if !errors.As(err, &detail) || detail.Field != sandbox.ExecFieldTimeout { + t.Fatalf("fractional timeout = %#v", err) + } +} diff --git a/sdks/go/sandbox/README.md b/sdks/go/sandbox/README.md index 737b76d1..8ccdc61d 100644 --- a/sdks/go/sandbox/README.md +++ b/sdks/go/sandbox/README.md @@ -34,11 +34,6 @@ if err != nil { log.Fatal(err) } fmt.Println(instance.ID()) - -files := instance.Filesystem() -if err := files.WriteFile(context.Background(), "/tmp/note.txt", []byte("hello\n"), nil); err != nil { - log.Fatal(err) -} ``` `Create` with a nil request uses the provider default when that default is @@ -49,6 +44,44 @@ does not stop, terminate, or delete the sandbox, and you can keep using the handle. The sandbox keeps running until you stop, terminate, archive, or delete it. +## Run a command + +```go +result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, +}) +if err != nil { + log.Fatal(err) +} +fmt.Println(result.GetExitCode(), string(result.GetStdout())) +``` + +Set exactly one of `Argv` or `Shell`. A non-zero exit code is not an SDK +error—check `GetExitCode`. Optional `WorkingDirectory` must be absolute. +`Environment` applies to that call only. Omit `Timeout` to use the client +deadline; use `Duration(0)` for no extra deadline on this call. Cancel the +context to stop waiting. See +[Process](https://github.com/sandbox-kit/kit/blob/main/docs/process.md). + +## Read and write files + +```go +files := instance.Filesystem() +if err := files.WriteFile(context.Background(), "/tmp/note.txt", []byte("hello\n"), nil); err != nil { + log.Fatal(err) +} +data, err := files.ReadFile(context.Background(), "/tmp/note.txt", nil) +if err != nil { + log.Fatal(err) +} +fmt.Print(string(data)) +``` + +Paths inside the sandbox are absolute. The same handle also supports +`CopyFromLocal`, `CopyToLocal`, `MakeDirectory`, `ListDirectory`, `Stat`, and +`Remove`. See +[Filesystem](https://github.com/sandbox-kit/kit/blob/main/docs/filesystem.md). + ## Choose a provider | Provider | Import | Open an existing sandbox | @@ -78,6 +111,8 @@ provider's own error when there is one. Runnable programs for each operation are in [examples/go](https://github.com/sandbox-kit/kit/tree/main/examples/go). +Provider packages ship `ExampleNew_*` samples for pkg.go.dev (create, +lifecycle, process, and filesystem). ## License diff --git a/sdks/go/sandbox/capabilities.go b/sdks/go/sandbox/capabilities.go index 8e3fe7b4..1e0d1391 100644 --- a/sdks/go/sandbox/capabilities.go +++ b/sdks/go/sandbox/capabilities.go @@ -81,6 +81,14 @@ type SandboxFiles interface { Remove(context.Context, string, string, *RemoveOptions) error } +// SandboxProcess runs a command in a sandbox and waits for it to finish. +// Daytona and Modal implement it. Use Sandbox.Process. +type SandboxProcess interface { + // RunCommand runs one command and returns its exit code and output. + // A non-zero exit code is not an error. + RunCommand(context.Context, string, *ExecOptions) (*ExecResult, error) +} + // SandboxDeleter deletes a sandbox. // Daytona implements it. Call Sandbox.Delete. Close does not delete the cloud sandbox. type SandboxDeleter interface { diff --git a/sdks/go/sandbox/client_contracts.go b/sdks/go/sandbox/client_contracts.go index 2f0edffe..2d846086 100644 --- a/sdks/go/sandbox/client_contracts.go +++ b/sdks/go/sandbox/client_contracts.go @@ -51,7 +51,7 @@ type ( ) // Duration converts a Go duration into the timeout value used by Config, -// lifecycle options, and Filesystem option messages. +// lifecycle options, Filesystem option messages, and Process.Run. func Duration(value time.Duration) *durationpb.Duration { return durationpb.New(value) } diff --git a/sdks/go/sandbox/clone.go b/sdks/go/sandbox/clone.go index d77e69e3..a9f935ab 100644 --- a/sdks/go/sandbox/clone.go +++ b/sdks/go/sandbox/clone.go @@ -26,6 +26,39 @@ func cloneSandboxInfo(info *SandboxInfo) *SandboxInfo { return out } +// cloneExecOptions returns an owned copy that preserves an explicit empty +// environment map. proto.Clone alone drops empty maps. +func cloneExecOptions(options *ExecOptions) *ExecOptions { + if options == nil { + return nil + } + out := proto.Clone(options).(*ExecOptions) + if options.Environment != nil && out.Environment == nil { + out.Environment = map[string]string{} + } + if options.Argv != nil && out.Argv == nil { + out.Argv = []string{} + } + return out +} + +// cloneExecResult returns an owned copy that preserves empty stdout and stderr +// slices. proto.Clone alone drops empty bytes, which would break the Daytona +// non-nil empty stderr contract. +func cloneExecResult(result *ExecResult) *ExecResult { + if result == nil { + return nil + } + out := proto.Clone(result).(*ExecResult) + if result.Stdout != nil && out.Stdout == nil { + out.Stdout = []byte{} + } + if result.Stderr != nil && out.Stderr == nil { + out.Stderr = []byte{} + } + return out +} + func preserveCreateOptionsCollections(src, dst *CreateOptions) { if src.Environment != nil && dst.Environment == nil { dst.Environment = map[string]string{} diff --git a/sdks/go/sandbox/clone_test.go b/sdks/go/sandbox/clone_test.go index bfc955e3..469bbb56 100644 --- a/sdks/go/sandbox/clone_test.go +++ b/sdks/go/sandbox/clone_test.go @@ -7,6 +7,26 @@ import ( "google.golang.org/protobuf/proto" ) +func TestCloneExecOptionsPreservesEmptyEnvironment(t *testing.T) { + options := &ExecOptions{Argv: []string{"echo"}, Environment: map[string]string{}} + cloned := cloneExecOptions(options) + if cloned.Environment == nil { + t.Fatal("explicit empty environment became nil") + } + options.Environment["X"] = "1" + if _, ok := cloned.Environment["X"]; ok { + t.Fatal("clone aliases original environment") + } +} + +func TestCloneExecResultPreservesEmptyOutput(t *testing.T) { + result := &ExecResult{ExitCode: 0, Stdout: []byte{}, Stderr: []byte{}} + cloned := cloneExecResult(result) + if cloned.Stdout == nil || cloned.Stderr == nil { + t.Fatalf("empty output became nil: stdout=%#v stderr=%#v", cloned.Stdout, cloned.Stderr) + } +} + func TestCopiedMetadataRetainsNumbersAndOwnership(t *testing.T) { big := uint64(math.MaxUint64) metadata := &MetadataObject{Values: map[string]*MetadataValue{ diff --git a/sdks/go/sandbox/doc.go b/sdks/go/sandbox/doc.go index 7ed7e601..73707752 100644 --- a/sdks/go/sandbox/doc.go +++ b/sdks/go/sandbox/doc.go @@ -6,7 +6,7 @@ // [Client.Close] and [Sandbox.Close] release local resources. They leave the // cloud sandbox running. Stop, terminate, archive, or delete it with a // lifecycle method the provider supports. Use [Sandbox.Filesystem] for file -// operations. +// operations and [Sandbox.Process] to run commands. // // When a call fails, use errors.As with [*Error]. Kind is the shared category. // Field names the setting to change, such as "resources.cpu_cores" or "endpoint". diff --git a/sdks/go/sandbox/example_test.go b/sdks/go/sandbox/example_test.go index 604124c9..7d9cd75b 100644 --- a/sdks/go/sandbox/example_test.go +++ b/sdks/go/sandbox/example_test.go @@ -1,6 +1,7 @@ package sandbox_test import ( + "context" "errors" "fmt" "time" @@ -69,3 +70,38 @@ func ExampleMetadataObjectAsMap() { // Output: // platform } + +func ExampleExecOptions() { + opts := &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, + Timeout: sandbox.Duration(5 * time.Second), + } + fmt.Println(opts.Argv[0], opts.Timeout.AsDuration()) + // Output: + // echo 5s +} + +func ExampleProcess_Run() { + // Obtain Process from a live Sandbox handle (Create, Get, or Attach). + // A nil handle fails before any provider call. + _, err := (*sandbox.Sandbox)(nil).Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, + }) + var detail *sandbox.Error + if errors.As(err, &detail) { + fmt.Println(detail.Kind) + } + // Output: + // ERROR_KIND_FAILED_PRECONDITION +} + +func ExampleFilesystem_WriteFile() { + // Obtain Filesystem from a live Sandbox handle (Create, Get, or Attach). + err := (*sandbox.Sandbox)(nil).Filesystem().WriteFile(context.Background(), "/tmp/note.txt", []byte("hello\n"), nil) + var detail *sandbox.Error + if errors.As(err, &detail) { + fmt.Println(detail.Kind) + } + // Output: + // ERROR_KIND_FAILED_PRECONDITION +} diff --git a/sdks/go/sandbox/field_paths.go b/sdks/go/sandbox/field_paths.go index b799198b..7b9713d1 100644 --- a/sdks/go/sandbox/field_paths.go +++ b/sdks/go/sandbox/field_paths.go @@ -380,3 +380,15 @@ const FileFieldLocalPath = "local_path" // FileFieldParents is the canonical path "parents" on MakeDirectoryOptions. const FileFieldParents = "parents" + +// ExecFieldArgv is the canonical path "argv" on ExecOptions. +const ExecFieldArgv = "argv" + +// ExecFieldShell is the canonical path "shell" on ExecOptions. +const ExecFieldShell = "shell" + +// ExecFieldWorkingDirectory is the canonical path "working_directory" on ExecOptions. +const ExecFieldWorkingDirectory = "working_directory" + +// ExecFieldTimeout is the canonical path "timeout" on ExecOptions. +const ExecFieldTimeout = "timeout" diff --git a/sdks/go/sandbox/filesystem.go b/sdks/go/sandbox/filesystem.go index 84431a72..367c6870 100644 --- a/sdks/go/sandbox/filesystem.go +++ b/sdks/go/sandbox/filesystem.go @@ -176,13 +176,17 @@ func (f *Filesystem) files(operation string) (SandboxFiles, error) { } func absoluteSandboxPath(owner *Sandbox, operation, path string) error { + return absolutePath(owner, operation, path, FileFieldPath, "sandbox-kit: sandbox path must be absolute") +} + +func absolutePath(owner *Sandbox, operation, path, field, message string) error { if path == "" || !strings.HasPrefix(path, "/") || strings.ContainsRune(path, 0) { return NewError(&ErrorInfo{ Kind: ErrorKindInvalidArgument, Provider: owner.ProviderName(), Operation: operation, - Field: FileFieldPath, - Message: "sandbox-kit: sandbox path must be absolute", + Field: field, + Message: message, }, nil) } return nil diff --git a/sdks/go/sandbox/process.go b/sdks/go/sandbox/process.go new file mode 100644 index 00000000..e180c3e5 --- /dev/null +++ b/sdks/go/sandbox/process.go @@ -0,0 +1,129 @@ +package sandbox + +import ( + "context" + "strings" + + "github.com/sandbox-kit/kit/sdks/go/internal/sdk" + durationpb "google.golang.org/protobuf/types/known/durationpb" +) + +// Process runs commands inside one sandbox and waits for each to finish. +// Obtain it from Sandbox.Process. Set exactly one of Argv or Shell. +// A working directory, when set, must be absolute. A nil timeout inherits +// Config.Timeout. Explicit zero adds no Kit deadline. +type Process struct { + owner *Sandbox +} + +// Process returns the command runner for this sandbox. +// Methods on a nil handle return ErrorKindFailedPrecondition. +func (s *Sandbox) Process() *Process { + if s == nil { + return &Process{} + } + return &Process{owner: s} +} + +// Run runs one command and returns its exit code and collected output. +// A non-zero exit code is not an error. Set exactly one of options.Argv or +// options.Shell. A nil context returns ErrorKindInvalidArgument. +func (p *Process) Run(ctx context.Context, options *ExecOptions) (*ExecResult, error) { + var result *ExecResult + err := p.call(ctx, "run", options, func(operationContext context.Context, id string) error { + runner, err := p.runner("run") + if err != nil { + return err + } + result, err = runner.RunCommand(operationContext, id, cloneExecOptions(options)) + return err + }) + if err != nil { + return nil, err + } + return cloneExecResult(result), nil +} + +func (p *Process) call(ctx context.Context, operation string, options *ExecOptions, invoke func(context.Context, string) error) error { + owner := p.owner + if owner == nil { + if ctx == nil { + return NewError(&ErrorInfo{Kind: ErrorKindInvalidArgument, Operation: operation, Field: "context", Message: "sandbox-kit: context is required"}, nil) + } + return NewError(&ErrorInfo{Kind: ErrorKindFailedPrecondition, Operation: operation, Message: "sandbox-kit: sandbox handle is not initialized"}, nil) + } + if err := validateExecOptions(owner, operation, options); err != nil { + return err + } + var timeout *durationpb.Duration + if options != nil { + timeout = options.Timeout + } + operationContext, cancel, id, err := owner.operationContext(ctx, operation, sdk.OptionalDuration(timeout)) + if err != nil { + return err + } + defer cancel() + if err := invoke(operationContext, id); err != nil { + return sdk.WithErrorContext(err, owner.ProviderName(), operation) + } + return nil +} + +func (p *Process) runner(operation string) (SandboxProcess, error) { + runner, ok := p.owner.operations.(SandboxProcess) + if !ok { + return nil, p.owner.unsupported(operation) + } + return runner, nil +} + +func validateExecOptions(owner *Sandbox, operation string, options *ExecOptions) error { + if options == nil { + return NewError(&ErrorInfo{ + Kind: ErrorKindInvalidArgument, + Provider: owner.ProviderName(), + Operation: operation, + Field: ExecFieldArgv, + Message: "sandbox-kit: set argv or shell", + }, nil) + } + hasArgv := len(options.Argv) > 0 + hasShell := options.Shell != nil + switch { + case hasArgv && hasShell: + return NewError(&ErrorInfo{ + Kind: ErrorKindInvalidArgument, + Provider: owner.ProviderName(), + Operation: operation, + Field: ExecFieldArgv, + Message: "sandbox-kit: set exactly one of argv or shell", + }, nil) + case !hasArgv && !hasShell: + return NewError(&ErrorInfo{ + Kind: ErrorKindInvalidArgument, + Provider: owner.ProviderName(), + Operation: operation, + Field: ExecFieldArgv, + Message: "sandbox-kit: set argv or shell", + }, nil) + case hasShell && strings.TrimSpace(*options.Shell) == "": + return NewError(&ErrorInfo{ + Kind: ErrorKindInvalidArgument, + Provider: owner.ProviderName(), + Operation: operation, + Field: ExecFieldShell, + Message: "sandbox-kit: shell must not be blank", + }, nil) + } + if options.WorkingDirectory != nil { + return absolutePath( + owner, + operation, + *options.WorkingDirectory, + ExecFieldWorkingDirectory, + "sandbox-kit: working directory must be absolute", + ) + } + return nil +} diff --git a/sdks/go/sandbox/sandbox.go b/sdks/go/sandbox/sandbox.go index db63ad18..776440b9 100644 --- a/sdks/go/sandbox/sandbox.go +++ b/sdks/go/sandbox/sandbox.go @@ -8,8 +8,8 @@ import ( // Sandbox is the handle for one cloud sandbox. // ID and Info describe it. Lifecycle methods change it. Filesystem returns the -// shared file operations. Close does not change the cloud sandbox. You can keep -// using the handle. +// shared file operations. Process runs commands. Close does not change the +// cloud sandbox. You can keep using the handle. // // Example: // @@ -94,7 +94,8 @@ func (s *Sandbox) Info() *SandboxInfo { } // Capabilities lists the lifecycle operations this provider implements. -// File operations are not lifecycle capabilities; call Filesystem instead. +// File and process operations are not lifecycle capabilities; call Filesystem +// or Process instead. // Pass a value to Supports, or compare it with LifecycleCapabilityPause and the other capability constants. func (s *Sandbox) Capabilities() []LifecycleCapability { if s == nil || s.operations == nil || s.currentID() == "" { @@ -116,7 +117,8 @@ func (s *Sandbox) currentID() string { } // Supports reports whether this provider implements a lifecycle capability. -// File operations are checked by calling Filesystem methods, not Supports. +// File and process operations are checked by calling Filesystem or Process +// methods, not Supports. func (s *Sandbox) Supports(capability LifecycleCapability) bool { for _, candidate := range s.Capabilities() { if candidate == capability { diff --git a/sdks/go/sandbox/sandbox_data_contracts.go b/sdks/go/sandbox/sandbox_data_contracts.go index d6107211..e5082fc9 100644 --- a/sdks/go/sandbox/sandbox_data_contracts.go +++ b/sdks/go/sandbox/sandbox_data_contracts.go @@ -149,6 +149,17 @@ type ( // Set Recursive with Value(true) to remove a directory and its contents. RemoveOptions = sandboxv1.RemoveOptions + // ExecOptions configures Process.Run. Set exactly one of Argv or Shell. + // An omitted Timeout uses Config.Timeout as the call deadline. Explicit + // zero adds no extra deadline for this call. Both omit and zero also mean + // no provider command limit (Daytona uses 0 so its 10s native default does + // not apply; Modal adds no command limit). Command timeouts must be whole + // seconds. + ExecOptions = sandboxv1.ExecOptions + + // ExecResult is the outcome of Process.Run. A non-zero ExitCode is not an error. + ExecResult = sandboxv1.ExecResult + // SandboxInfo is the metadata for one sandbox. // Sandbox.Info returns a copy. State is a SandboxState. SandboxInfo = sandboxv1.SandboxInfo @@ -212,7 +223,8 @@ type ( SandboxState = sandboxv1.SandboxState // LifecycleCapability names one lifecycle operation Sandbox.Supports can check. - // File operations are not lifecycle capabilities; call Filesystem instead. + // File and process operations are not lifecycle capabilities; call + // Filesystem or Process instead. LifecycleCapability = sandboxv1.LifecycleCapability ) diff --git a/sdks/go/sandbox/tests/process_test.go b/sdks/go/sandbox/tests/process_test.go new file mode 100644 index 00000000..de3e2879 --- /dev/null +++ b/sdks/go/sandbox/tests/process_test.go @@ -0,0 +1,346 @@ +package sandbox_test + +import ( + "context" + "errors" + "sync/atomic" + "testing" + "time" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +type processBackend struct { + run func(context.Context, string, *sandbox.ExecOptions) (*sandbox.ExecResult, error) +} + +func (*processBackend) Name() string { return "process" } +func (*processBackend) Close(context.Context) error { return nil } +func (*processBackend) CloseSandbox(context.Context, string) error { + return nil +} +func (*processBackend) Create(context.Context, *sandbox.CreateOptions) (*sandbox.CreateResult, error) { + return &sandbox.CreateResult{Sandbox: &sandbox.SandboxInfo{Id: "created"}}, nil +} +func (b *processBackend) RunCommand(ctx context.Context, id string, options *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + return b.run(ctx, id, options) +} + +type processProvider struct{ backend sandbox.Backend } + +func (*processProvider) Name() string { return "process" } +func (p *processProvider) NewClient(*sandbox.Config) (sandbox.Backend, error) { + return p.backend, nil +} + +func TestProcessNilHandle(t *testing.T) { + _, err := (*sandbox.Sandbox)(nil).Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hi"}, + }) + var detail *sandbox.Error + if !errors.As(err, &detail) || detail.Kind != sandbox.ErrorKindFailedPrecondition { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestProcessRejectsMissingCommand(t *testing.T) { + calls := 0 + backend := &processBackend{run: func(context.Context, string, *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + calls++ + return &sandbox.ExecResult{}, nil + }} + client, err := sandbox.NewClient(&processProvider{backend: backend}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + _, err = instance.Process().Run(context.Background(), nil) + var detail *sandbox.Error + if !errors.As(err, &detail) || detail.Kind != sandbox.ErrorKindInvalidArgument || detail.Field != sandbox.ExecFieldArgv || detail.Operation != "run" { + t.Fatalf("unexpected error: %v", err) + } + if calls != 0 { + t.Fatal("provider was called without a command") + } +} + +func TestProcessRejectsBothArgvAndShell(t *testing.T) { + client, err := sandbox.NewClient(&processProvider{backend: &processBackend{ + run: func(context.Context, string, *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + t.Fatal("provider was called") + return nil, nil + }, + }}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + _, err = instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo"}, + Shell: sandbox.Value("echo hi"), + }) + var detail *sandbox.Error + if !errors.As(err, &detail) || detail.Kind != sandbox.ErrorKindInvalidArgument || detail.Field != sandbox.ExecFieldArgv { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestProcessRejectsBlankShell(t *testing.T) { + client, err := sandbox.NewClient(&processProvider{backend: &processBackend{ + run: func(context.Context, string, *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + t.Fatal("provider was called") + return nil, nil + }, + }}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + for _, shell := range []string{"", " "} { + _, err = instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Shell: sandbox.Value(shell), + }) + var detail *sandbox.Error + if !errors.As(err, &detail) || detail.Kind != sandbox.ErrorKindInvalidArgument || detail.Field != sandbox.ExecFieldShell || detail.Operation != "run" { + t.Fatalf("shell %q: unexpected error: %v", shell, err) + } + } +} + +func TestProcessRejectsRelativeWorkingDirectory(t *testing.T) { + client, err := sandbox.NewClient(&processProvider{backend: &processBackend{ + run: func(context.Context, string, *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + t.Fatal("provider was called") + return nil, nil + }, + }}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + _, err = instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"pwd"}, + WorkingDirectory: sandbox.Value("relative"), + }) + var detail *sandbox.Error + if !errors.As(err, &detail) || detail.Kind != sandbox.ErrorKindInvalidArgument || detail.Field != sandbox.ExecFieldWorkingDirectory { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestProcessRunsArgv(t *testing.T) { + backend := &processBackend{run: func(_ context.Context, id string, options *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + if id != "created" || len(options.Argv) != 2 || options.Argv[0] != "echo" || options.Argv[1] != "hello" { + t.Fatalf("unexpected run %s %#v", id, options) + } + return &sandbox.ExecResult{ExitCode: 0, Stdout: []byte("hello\n")}, nil + }} + client, err := sandbox.NewClient(&processProvider{backend: backend}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello"}, + }) + if err != nil { + t.Fatal(err) + } + if result.GetExitCode() != 0 || string(result.GetStdout()) != "hello\n" { + t.Fatalf("unexpected result %#v", result) + } +} + +func TestProcessPreservesExplicitEmptyEnvironment(t *testing.T) { + backend := &processBackend{run: func(_ context.Context, _ string, options *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + if options.Environment == nil { + t.Fatal("explicit empty environment became nil") + } + return &sandbox.ExecResult{ExitCode: 0, Stdout: []byte{}, Stderr: []byte{}}, nil + }} + client, err := sandbox.NewClient(&processProvider{backend: backend}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"true"}, + Environment: map[string]string{}, + }) + if err != nil { + t.Fatal(err) + } + if result.Stdout == nil || result.Stderr == nil { + t.Fatalf("empty output became nil: stdout=%#v stderr=%#v", result.Stdout, result.Stderr) + } +} + +func TestProcessNonZeroExitIsSuccess(t *testing.T) { + backend := &processBackend{run: func(context.Context, string, *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + return &sandbox.ExecResult{ExitCode: 7, Stderr: []byte("boom\n")}, nil + }} + client, err := sandbox.NewClient(&processProvider{backend: backend}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Shell: sandbox.Value("exit 7"), + }) + if err != nil { + t.Fatal(err) + } + if result.GetExitCode() != 7 { + t.Fatalf("unexpected exit %d", result.GetExitCode()) + } +} + +func TestProcessUnsupportedWithoutCapability(t *testing.T) { + client, err := sandbox.NewClient(&lifecycleProvider{backend: &lifecycleBackend{}}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + _, err = instance.Process().Run(context.Background(), &sandbox.ExecOptions{Argv: []string{"true"}}) + var detail *sandbox.Error + if !errors.As(err, &detail) || detail.Kind != sandbox.ErrorKindUnsupported || detail.Operation != "run" { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestProcessCancelEndsWait(t *testing.T) { + started := make(chan struct{}) + backend := &processBackend{run: func(ctx context.Context, _ string, _ *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + close(started) + <-ctx.Done() + return nil, ctx.Err() + }} + client, err := sandbox.NewClient(&processProvider{backend: backend}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error, 1) + go func() { + _, runErr := instance.Process().Run(ctx, &sandbox.ExecOptions{Argv: []string{"sleep", "1"}}) + errCh <- runErr + }() + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatal("provider did not start") + } + cancel() + runErr := <-errCh + var detail *sandbox.Error + if !errors.Is(runErr, context.Canceled) || !errors.As(runErr, &detail) || detail.Kind != sandbox.ErrorKindCanceled || detail.Operation != "run" { + t.Fatalf("unexpected error: %v", runErr) + } +} + +func TestProcessHoldsSerializationUntilCanceledInFlightSettles(t *testing.T) { + var active atomic.Int32 + var maxActive atomic.Int32 + releaseFirst := make(chan struct{}) + firstStarted := make(chan struct{}) + var firstCall atomic.Bool + + backend := &processBackend{run: func(ctx context.Context, _ string, _ *sandbox.ExecOptions) (*sandbox.ExecResult, error) { + n := active.Add(1) + for { + old := maxActive.Load() + if n <= old || maxActive.CompareAndSwap(old, n) { + break + } + } + defer active.Add(-1) + + if firstCall.CompareAndSwap(false, true) { + close(firstStarted) + <-releaseFirst + if err := ctx.Err(); err != nil { + return nil, err + } + } + return &sandbox.ExecResult{ExitCode: 0, Stdout: []byte{}, Stderr: []byte{}}, nil + }} + client, err := sandbox.NewClient(&processProvider{backend: backend}, &sandbox.Config{}) + if err != nil { + t.Fatal(err) + } + instance, err := client.Create(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + firstErr := make(chan error, 1) + go func() { + _, runErr := instance.Process().Run(ctx, &sandbox.ExecOptions{Argv: []string{"sleep", "1"}}) + firstErr <- runErr + }() + select { + case <-firstStarted: + case <-time.After(2 * time.Second): + t.Fatal("first provider call did not start") + } + cancel() + + secondErr := make(chan error, 1) + go func() { + _, runErr := instance.Process().Run(context.Background(), &sandbox.ExecOptions{Argv: []string{"true"}}) + secondErr <- runErr + }() + + deadline := time.Now().Add(200 * time.Millisecond) + for time.Now().Before(deadline) { + if active.Load() != 1 { + t.Fatalf("expected one in-flight provider call, got %d", active.Load()) + } + if maxActive.Load() != 1 { + t.Fatalf("expected max concurrency 1, got %d", maxActive.Load()) + } + time.Sleep(5 * time.Millisecond) + } + + close(releaseFirst) + runErr := <-firstErr + var detail *sandbox.Error + if !errors.Is(runErr, context.Canceled) || !errors.As(runErr, &detail) || detail.Kind != sandbox.ErrorKindCanceled { + t.Fatalf("first call: unexpected error: %v", runErr) + } + if err := <-secondErr; err != nil { + t.Fatalf("second call: %v", err) + } + if maxActive.Load() != 1 { + t.Fatalf("expected max concurrency 1, got %d", maxActive.Load()) + } +} diff --git a/sdks/typescript/contracts/src/kit/sandbox/v1/sandbox_pb.ts b/sdks/typescript/contracts/src/kit/sandbox/v1/sandbox_pb.ts index 94c65264..fb10b275 100644 --- a/sdks/typescript/contracts/src/kit/sandbox/v1/sandbox_pb.ts +++ b/sdks/typescript/contracts/src/kit/sandbox/v1/sandbox_pb.ts @@ -13,7 +13,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file kit/sandbox/v1/sandbox.proto. */ export const file_kit_sandbox_v1_sandbox: GenFile = /*@__PURE__*/ - fileDesc("ChxraXQvc2FuZGJveC92MS9zYW5kYm94LnByb3RvEg5raXQuc2FuZGJveC52MSLdBgoNQ3JlYXRlT3B0aW9ucxIRCgRuYW1lGAEgASgJSACIAQESLQoGc291cmNlGAIgASgLMh0ua2l0LnNhbmRib3gudjEuU2FuZGJveFNvdXJjZRIuCgdydW50aW1lGAMgASgLMh0ua2l0LnNhbmRib3gudjEuUnVudGltZUNvbmZpZxIyCglpc29sYXRpb24YBCABKAsyHy5raXQuc2FuZGJveC52MS5Jc29sYXRpb25Db25maWcSLAoJcmVzb3VyY2VzGAUgASgLMhkua2l0LnNhbmRib3gudjEuUmVzb3VyY2VzEiwKCXBsYWNlbWVudBgGIAEoCzIZLmtpdC5zYW5kYm94LnYxLlBsYWNlbWVudBJDCgtlbnZpcm9ubWVudBgHIAMoCzIuLmtpdC5zYW5kYm94LnYxLkNyZWF0ZU9wdGlvbnMuRW52aXJvbm1lbnRFbnRyeRI5CgZsYWJlbHMYCCADKAsyKS5raXQuc2FuZGJveC52MS5DcmVhdGVPcHRpb25zLkxhYmVsc0VudHJ5Ei4KB25ldHdvcmsYCSABKAsyHS5raXQuc2FuZGJveC52MS5OZXR3b3JrQ29uZmlnEjAKCHNlY3VyaXR5GAogASgLMh4ua2l0LnNhbmRib3gudjEuU2VjdXJpdHlDb25maWcSMAoIbGlmZXRpbWUYCyABKAsyHi5raXQuc2FuZGJveC52MS5MaWZldGltZVBvbGljeRJDCgxwcm92aXNpb25pbmcYDCABKAsyIy5raXQuc2FuZGJveC52MS5Qcm92aXNpb25pbmdPcHRpb25zUghjcmVhdGlvbhIxCglyZWFkaW5lc3MYDSABKAsyHi5raXQuc2FuZGJveC52MS5SZWFkaW5lc3NQcm9iZRI6Cg1vYnNlcnZhYmlsaXR5GA4gASgLMiMua2l0LnNhbmRib3gudjEuT2JzZXJ2YWJpbGl0eUNvbmZpZxoyChBFbnZpcm9ubWVudEVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIHCgVfbmFtZUoECA8QEFIQcHJvdmlkZXJfb3B0aW9ucyLxAQoNU2FuZGJveFNvdXJjZRJBChBwcm92aWRlcl9kZWZhdWx0GAEgASgLMiUua2l0LnNhbmRib3gudjEuUHJvdmlkZXJEZWZhdWx0U291cmNlSAASLAoFaW1hZ2UYAiABKAsyGy5raXQuc2FuZGJveC52MS5JbWFnZVNvdXJjZUgAEjIKCHNuYXBzaG90GAMgASgLMh4ua2l0LnNhbmRib3gudjEuU25hcHNob3RTb3VyY2VIABIzCgl3YXJtX3Bvb2wYBCABKAsyHi5raXQuc2FuZGJveC52MS5XYXJtUG9vbFNvdXJjZUgAQgYKBGtpbmQiFwoVUHJvdmlkZXJEZWZhdWx0U291cmNlIo4DCgtJbWFnZVNvdXJjZRL+AgoJcmVmZXJlbmNlGAEgASgJQuoCukjmAroBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJ7oBrwIKD2ltYWdlX3JlZmVyZW5jZRIjbXVzdCBiZSBhIGNvbnRhaW5lciBpbWFnZSByZWZlcmVuY2Ua9gF0aGlzLnNpemUoKSA8PSA1MTIgJiYgdGhpcy5tYXRjaGVzKCdeKD86W0EtWmEtejAtOV0oPzpbQS1aYS16MC05Li1dKltBLVphLXowLTldKT8oPzo6WzAtOV0rKT8vKT9bYS16MC05XSsoPzooPzpbLl9dfF9ffC0rKVthLXowLTldKykqKD86L1thLXowLTldKyg/Oig/OlsuX118X198LSspW2EtejAtOV0rKSopKig/OjpbQS1aYS16MC05X11bQS1aYS16MC05Xy4tXXswLDEyN30pPyg/OkBzaGEyNTY6W2EtZkEtRjAtOV17NjR9KT8kJykivwEKDlNuYXBzaG90U291cmNlEkkKCXJlZmVyZW5jZRgBIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnEjQKBGtpbmQYAiABKA4yHC5raXQuc2FuZGJveC52MS5TbmFwc2hvdEtpbmRCCLpIBYIBAhABEhoKDXByb3ZpZGVyX2tpbmQYAyABKAlIAIgBAUIQCg5fcHJvdmlkZXJfa2luZCJbCg5XYXJtUG9vbFNvdXJjZRJJCglyZWZlcmVuY2UYASABKAlCNrpIM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJyLyAwoNUnVudGltZUNvbmZpZxJNCghsYW5ndWFnZRgBIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSACIAQESTAoHdmVyc2lvbhgCIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSAGIAQESSQoEdXNlchgDIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSAKIAQESVgoRd29ya2luZ19kaXJlY3RvcnkYBCABKAlCNrpIM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJ0gDiAEBEk8KCmVudHJ5cG9pbnQYBSADKAlCO7pIOJIBNSIzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnEhAKA3B0eRgGIAEoCEgEiAEBQgsKCV9sYW5ndWFnZUIKCghfdmVyc2lvbkIHCgVfdXNlckIUChJfd29ya2luZ19kaXJlY3RvcnlCBgoEX3B0eSK0AQoPSXNvbGF0aW9uQ29uZmlnEjUKBGtpbmQYASABKA4yHS5raXQuc2FuZGJveC52MS5Jc29sYXRpb25LaW5kQgi6SAWCAQIQARIaCg1wcm92aWRlcl9raW5kGAIgASgJSACIAQESIgoVbmVzdGVkX3ZpcnR1YWxpemF0aW9uGAMgASgISAGIAQFCEAoOX3Byb3ZpZGVyX2tpbmRCGAoWX25lc3RlZF92aXJ0dWFsaXphdGlvbiKTCwoJUmVzb3VyY2VzEpABCgljcHVfY29yZXMYASABKAFCeLpIdboBOAoGZmluaXRlEg5tdXN0IGJlIGZpbml0ZRoeIXRoaXMuaXNOYW4oKSAmJiAhdGhpcy5pc0luZigpugE3ChFleGNsdXNpdmVfbWluaW11bRIWbXVzdCBiZSBncmVhdGVyIHRoYW4gMBoKdGhpcyA+IDAuMEgAiAEBEokBCg9jcHVfbGltaXRfY29yZXMYAiABKAFCa7pIaLoBOAoGZmluaXRlEg5tdXN0IGJlIGZpbml0ZRoeIXRoaXMuaXNOYW4oKSAmJiAhdGhpcy5pc0luZigpugEqCgdtaW5pbXVtEhJtdXN0IGJlIGF0IGxlYXN0IDAaC3RoaXMgPj0gMC4wSAGIAQESVQoKbWVtb3J5X21pYhgDIAEoBEI8ukg5ugE2ChFleGNsdXNpdmVfbWluaW11bRIWbXVzdCBiZSBncmVhdGVyIHRoYW4gMBoJdGhpcyA+IDB1SAKIAQESHQoQbWVtb3J5X2xpbWl0X21pYhgEIAEoBEgDiAEBElMKCGRpc2tfbWliGAUgASgEQjy6SDm6ATYKEWV4Y2x1c2l2ZV9taW5pbXVtEhZtdXN0IGJlIGdyZWF0ZXIgdGhhbiAwGgl0aGlzID4gMHVIBIgBATrIBrpIxAYazwEKCWNwdV9saW1pdBJFY3B1X2xpbWl0X2NvcmVzIG11c3QgYmUgYXQgbGVhc3QgY3B1X2NvcmVzIHdoZW4gdGhlIGxpbWl0IGlzIHBvc2l0aXZlGnshaGFzKHRoaXMuY3B1X2xpbWl0X2NvcmVzKSB8fCB0aGlzLmNwdV9saW1pdF9jb3JlcyA8PSAwLjAgfHwgIWhhcyh0aGlzLmNwdV9jb3JlcykgfHwgdGhpcy5jcHVfbGltaXRfY29yZXMgPj0gdGhpcy5jcHVfY29yZXMa2AEKDG1lbW9yeV9saW1pdBJHbWVtb3J5X2xpbWl0X21pYiBtdXN0IGJlIGF0IGxlYXN0IG1lbW9yeV9taWIgd2hlbiB0aGUgbGltaXQgaXMgcG9zaXRpdmUafyFoYXModGhpcy5tZW1vcnlfbGltaXRfbWliKSB8fCB0aGlzLm1lbW9yeV9saW1pdF9taWIgPT0gMHUgfHwgIWhhcyh0aGlzLm1lbW9yeV9taWIpIHx8IHRoaXMubWVtb3J5X2xpbWl0X21pYiA+PSB0aGlzLm1lbW9yeV9taWIaxQEKFWNwdV9yZXF1aXJlc19wb3NpdGl2ZRJAYSBwb3NpdGl2ZSBjcHVfbGltaXRfY29yZXMgcmVxdWlyZXMgYSBwb3NpdGl2ZSBjcHVfY29yZXMgcmVxdWVzdBpqIWhhcyh0aGlzLmNwdV9saW1pdF9jb3JlcykgfHwgdGhpcy5jcHVfbGltaXRfY29yZXMgPD0gMC4wIHx8IChoYXModGhpcy5jcHVfY29yZXMpICYmIHRoaXMuY3B1X2NvcmVzID4gMC4wKRrMAQoYbWVtb3J5X3JlcXVpcmVzX3Bvc2l0aXZlEkJhIHBvc2l0aXZlIG1lbW9yeV9saW1pdF9taWIgcmVxdWlyZXMgYSBwb3NpdGl2ZSBtZW1vcnlfbWliIHJlcXVlc3QabCFoYXModGhpcy5tZW1vcnlfbGltaXRfbWliKSB8fCB0aGlzLm1lbW9yeV9saW1pdF9taWIgPT0gMHUgfHwgKGhhcyh0aGlzLm1lbW9yeV9taWIpICYmIHRoaXMubWVtb3J5X21pYiA+IDB1KUIMCgpfY3B1X2NvcmVzQhIKEF9jcHVfbGltaXRfY29yZXNCDQoLX21lbW9yeV9taWJCEwoRX21lbW9yeV9saW1pdF9taWJCCwoJX2Rpc2tfbWliIpcBCglQbGFjZW1lbnQSEgoFY2xvdWQYASABKAlIAIgBARJMCgdyZWdpb25zGAIgAygJQju6SDiSATUiM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJxITCgZ0YXJnZXQYAyABKAlIAYgBAUIICgZfY2xvdWRCCQoHX3RhcmdldCIiCg9TdHJpbmdBbGxvd2xpc3QSDwoHZW50cmllcxgBIAMoCSKXCgoNTmV0d29ya0NvbmZpZxI0CgZlZ3Jlc3MYASABKA4yGi5raXQuc2FuZGJveC52MS5FZ3Jlc3NNb2RlQgi6SAWCAQIQARI3Cg5vdXRib3VuZF9jaWRycxgCIAEoCzIfLmtpdC5zYW5kYm94LnYxLlN0cmluZ0FsbG93bGlzdBI5ChBvdXRib3VuZF9kb21haW5zGAMgASgLMh8ua2l0LnNhbmRib3gudjEuU3RyaW5nQWxsb3dsaXN0EjYKDWluYm91bmRfY2lkcnMYBCABKAsyHy5raXQuc2FuZGJveC52MS5TdHJpbmdBbGxvd2xpc3QSHAoPcHJpdmF0ZV9uZXR3b3JrGAUgASgISACIAQESUwoObGlua2VkX3NhbmRib3gYBiABKAlCNrpIM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJ0gBiAEBElQKD3Byb3h5X3JlZmVyZW5jZRgHIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSAKIAQESHwoSb3V0Ym91bmRfcHJveHlfdXJsGAggASgJSAOIAQESGgoNY3VzdG9tX2RvbWFpbhgJIAEoCUgEiAEBEioKBXBvcnRzGAogAygLMhsua2l0LnNhbmRib3gudjEuUG9ydEJpbmRpbmc6jQW6SIkFGqoBCh1yZXN0cmljdGVkX3JlcXVpcmVzX2FsbG93bGlzdBI9cmVzdHJpY3RlZCBlZ3Jlc3MgcmVxdWlyZXMgb3V0Ym91bmRfY2lkcnMgb3Igb3V0Ym91bmRfZG9tYWlucxpKdGhpcy5lZ3Jlc3MgIT0gMyB8fCBoYXModGhpcy5vdXRib3VuZF9jaWRycykgfHwgaGFzKHRoaXMub3V0Ym91bmRfZG9tYWlucykaugIKGWJsb2NrX2FsbF9mb3JiaWRzX25ldHdvcmsSQGJsb2NrLWFsbCBlZ3Jlc3MgZm9yYmlkcyBuZXR3b3JraW5nIGFsbG93bGlzdHMsIGxpbmtzLCBhbmQgcG9ydHMa2gF0aGlzLmVncmVzcyAhPSAyIHx8ICghaGFzKHRoaXMub3V0Ym91bmRfY2lkcnMpICYmICFoYXModGhpcy5vdXRib3VuZF9kb21haW5zKSAmJiAhaGFzKHRoaXMuaW5ib3VuZF9jaWRycykgJiYgIWhhcyh0aGlzLmxpbmtlZF9zYW5kYm94KSAmJiB0aGlzLnBvcnRzLnNpemUoKSA9PSAwICYmICghaGFzKHRoaXMucHJpdmF0ZV9uZXR3b3JrKSB8fCAhdGhpcy5wcml2YXRlX25ldHdvcmspKRqcAQocYWxsb3dfYWxsX2ZvcmJpZHNfYWxsb3dsaXN0cxIsYWxsb3ctYWxsIGVncmVzcyBmb3JiaWRzIG91dGJvdW5kIGFsbG93bGlzdHMaTnRoaXMuZWdyZXNzICE9IDEgfHwgKCFoYXModGhpcy5vdXRib3VuZF9jaWRycykgJiYgIWhhcyh0aGlzLm91dGJvdW5kX2RvbWFpbnMpKUISChBfcHJpdmF0ZV9uZXR3b3JrQhEKD19saW5rZWRfc2FuZGJveEISChBfcHJveHlfcmVmZXJlbmNlQhUKE19vdXRib3VuZF9wcm94eV91cmxCEAoOX2N1c3RvbV9kb21haW4ijwEKC1BvcnRCaW5kaW5nEkQKBHBvcnQYASABKA1CNrpIM7oBMAoHbWF4aW11bRIVbXVzdCBiZSBhdCBtb3N0IDY1NTM1Gg50aGlzIDw9IDY1NTM1dRI6Cgl0cmFuc3BvcnQYAiABKA4yHS5raXQuc2FuZGJveC52MS5Qb3J0VHJhbnNwb3J0Qgi6SAWCAQIQASKmAQoOU2VjdXJpdHlDb25maWcSGgoNcHVibGljX2FjY2VzcxgBIAEoCEgAiAEBEh4KEXdvcmtsb2FkX2lkZW50aXR5GAIgASgISAGIAQESMAoHc2VjcmV0cxgDIAMoCzIfLmtpdC5zYW5kYm94LnYxLlNlY3JldFJlZmVyZW5jZUIQCg5fcHVibGljX2FjY2Vzc0IUChJfd29ya2xvYWRfaWRlbnRpdHkijgIKD1NlY3JldFJlZmVyZW5jZRJJCglyZWZlcmVuY2UYASABKAlCNrpIM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJxJZChRlbnZpcm9ubWVudF92YXJpYWJsZRgCIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSACIAQESPAoJaW5qZWN0aW9uGAMgASgOMh8ua2l0LnNhbmRib3gudjEuU2VjcmV0SW5qZWN0aW9uQgi6SAWCAQIQAUIXChVfZW52aXJvbm1lbnRfdmFyaWFibGUiigMKDkxpZmV0aW1lUG9saWN5Ej0KEG1heGltdW1fbGlmZXRpbWUYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CCLpIBaoBAjIAEhYKCWVwaGVtZXJhbBgCIAEoCEgAiAEBEjcKDmlkbGVfdGVybWluYXRlGAMgASgLMh8ua2l0LnNhbmRib3gudjEuQXV0b21hdGljQWN0aW9uEjIKCWlkbGVfc3RvcBgEIAEoCzIfLmtpdC5zYW5kYm94LnYxLkF1dG9tYXRpY0FjdGlvbhIzCgppZGxlX3BhdXNlGAUgASgLMh8ua2l0LnNhbmRib3gudjEuQXV0b21hdGljQWN0aW9uEjgKD3N0b3BwZWRfYXJjaGl2ZRgGIAEoCzIfLmtpdC5zYW5kYm94LnYxLkF1dG9tYXRpY0FjdGlvbhI3Cg5zdG9wcGVkX2RlbGV0ZRgHIAEoCzIfLmtpdC5zYW5kYm94LnYxLkF1dG9tYXRpY0FjdGlvbkIMCgpfZXBoZW1lcmFsIr0CCg9BdXRvbWF0aWNBY3Rpb24SMgoEbW9kZRgBIAEoDjIaLmtpdC5zYW5kYm94LnYxLlBvbGljeU1vZGVCCLpIBYIBAhABEjIKBWFmdGVyGAIgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQgi6SAWqAQIyADrBAbpIvQEaVwoXYWZ0ZXJfcmVxdWlyZXNfZHVyYXRpb24SGW1vZGUgYWZ0ZXIgcmVxdWlyZXMgYWZ0ZXIaIXRoaXMubW9kZSAhPSAyIHx8IGhhcyh0aGlzLmFmdGVyKRpiChZhZnRlcl9mb3JiaWRzX2R1cmF0aW9uEiRhZnRlciBpcyBvbmx5IHNldCB3aGVuIG1vZGUgaXMgYWZ0ZXIaInRoaXMubW9kZSA9PSAyIHx8ICFoYXModGhpcy5hZnRlcikiwgEKE1Byb3Zpc2lvbmluZ09wdGlvbnMSNAoHdGltZW91dBgBIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkIIukgFqgECMgASOgoNcXVldWVfdGltZW91dBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkIIukgFqgECMgASOQoId2FpdF9mb3IYAyABKA4yHS5raXQuc2FuZGJveC52MS5XYWl0Q29uZGl0aW9uQgi6SAWCAQIQASLJAQoOUmVhZGluZXNzUHJvYmUSdgoIdGNwX3BvcnQYASABKA1CYrpIX7oBKQoHbWluaW11bRISbXVzdCBiZSBhdCBsZWFzdCAxGgp0aGlzID49IDF1ugEwCgdtYXhpbXVtEhVtdXN0IGJlIGF0IG1vc3QgNjU1MzUaDnRoaXMgPD0gNjU1MzV1SAASLwoHY29tbWFuZBgCIAEoCzIcLmtpdC5zYW5kYm94LnYxLkNvbW1hbmRQcm9iZUgAQg4KBWNoZWNrEgW6SAIIASJlCgxDb21tYW5kUHJvYmUSVQoEYXJndhgBIAMoCUJHukhEugFBCgltaW5faXRlbXMSIm11c3QgY29udGFpbiBhdCBsZWFzdCBvbmUgYXJndW1lbnQaEHRoaXMuc2l6ZSgpID49IDEipwEKE09ic2VydmFiaWxpdHlDb25maWcSFAoHdmVyYm9zZRgBIAEoCEgAiAEBElcKEnRlbGVtZXRyeV9lbmRwb2ludBgCIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSAGIAQFCCgoIX3ZlcmJvc2VCFQoTX3RlbGVtZXRyeV9lbmRwb2ludCI8CgxDcmVhdGVSZXN1bHQSLAoHc2FuZGJveBgBIAEoCzIbLmtpdC5zYW5kYm94LnYxLlNhbmRib3hJbmZvIjoKDFN0YXJ0T3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIlcKC1N0b3BPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24SEgoFZm9yY2UYAiABKAhIAIgBAUIICgZfZm9yY2UiOgoMUGF1c2VPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iWgoQVGVybWluYXRlT3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEhEKBHdhaXQYAiABKAhIAIgBAUIHCgVfd2FpdCI8Cg5BcmNoaXZlT3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIlcKDURlbGV0ZU9wdGlvbnMSKgoHdGltZW91dBgBIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhIRCgR3YWl0GAIgASgISACIAQFCBwoFX3dhaXQiMgoKRXhpdFJlc3VsdBIWCglleGl0X2NvZGUYASABKAVIAIgBAUIMCgpfZXhpdF9jb2RlIskCCghGaWxlSW5mbxIMCgRuYW1lGAEgASgJEgwKBHBhdGgYAiABKAkSMAoEdHlwZRgDIAEoDjIYLmtpdC5zYW5kYm94LnYxLkZpbGVUeXBlQgi6SAWCAQIQARIMCgRzaXplGAQgASgDEhEKBG1vZGUYBSABKAlIAIgBARIYCgtwZXJtaXNzaW9ucxgGIAEoCUgBiAEBEhIKBW93bmVyGAcgASgJSAKIAQESEgoFZ3JvdXAYCCABKAlIA4gBARIvCgttb2RpZmllZF9hdBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoOc3ltbGlua190YXJnZXQYCiABKAlIBIgBAUIHCgVfbW9kZUIOCgxfcGVybWlzc2lvbnNCCAoGX293bmVyQggKBl9ncm91cEIRCg9fc3ltbGlua190YXJnZXQiPQoPRmlsZVJlYWRPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iPgoQRmlsZVdyaXRlT3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIj0KD0ZpbGVDb3B5T3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uImQKFE1ha2VEaXJlY3RvcnlPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24SFAoHcGFyZW50cxgCIAEoCEgAiAEBQgoKCF9wYXJlbnRzIkIKFExpc3REaXJlY3RvcnlPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iOQoLU3RhdE9wdGlvbnMSKgoHdGltZW91dBgBIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbiJhCg1SZW1vdmVPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24SFgoJcmVjdXJzaXZlGAIgASgISACIAQFCDAoKX3JlY3Vyc2l2ZSKsBAoLU2FuZGJveEluZm8SCgoCaWQYASABKAkSEAoIcHJvdmlkZXIYAiABKAkSEQoEbmFtZRgDIAEoCUgAiAEBEhsKDnByb3ZpZGVyX3N0YXRlGAQgASgJSAGIAQESEwoGcmVnaW9uGAUgASgJSAKIAQESMgoJaXNvbGF0aW9uGAYgASgLMh8ua2l0LnNhbmRib3gudjEuSXNvbGF0aW9uQ29uZmlnEiwKCXJlc291cmNlcxgHIAEoCzIZLmtpdC5zYW5kYm94LnYxLlJlc291cmNlcxI3CgZsYWJlbHMYCCADKAsyJy5raXQuc2FuZGJveC52MS5TYW5kYm94SW5mby5MYWJlbHNFbnRyeRIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBI5ChFwcm92aWRlcl9tZXRhZGF0YRgKIAEoCzIeLmtpdC5zYW5kYm94LnYxLk1ldGFkYXRhT2JqZWN0EisKBXN0YXRlGAwgASgOMhwua2l0LnNhbmRib3gudjEuU2FuZGJveFN0YXRlEhUKCHRlcm1pbmFsGA0gASgISAOIAQEaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIHCgVfbmFtZUIRCg9fcHJvdmlkZXJfc3RhdGVCCQoHX3JlZ2lvbkILCglfdGVybWluYWxKBAgLEAxSB29yaWdpbnMinwIKCUVycm9ySW5mbxInCgRraW5kGAEgASgOMhkua2l0LnNhbmRib3gudjEuRXJyb3JLaW5kEhAKCHByb3ZpZGVyGAIgASgJEhEKCW9wZXJhdGlvbhgDIAEoCRINCgVmaWVsZBgEIAEoCRIPCgdtZXNzYWdlGAUgASgJEhgKC3N0YXR1c19jb2RlGAYgASgNSACIAQESGgoNcHJvdmlkZXJfY29kZRgHIAEoCUgBiAEBEhwKD3Byb3ZpZGVyX3NvdXJjZRgIIAEoCUgCiAEBEhEKBHJ1bGUYCSABKAlIA4gBAUIOCgxfc3RhdHVzX2NvZGVCEAoOX3Byb3ZpZGVyX2NvZGVCEgoQX3Byb3ZpZGVyX3NvdXJjZUIHCgVfcnVsZSL1AQoNTWV0YWRhdGFWYWx1ZRIRCgdib29sZWFuGAEgASgISAASDgoEdGV4dBgCIAEoCUgAEhgKDnNpZ25lZF9pbnRlZ2VyGAMgASgDSAASGgoQdW5zaWduZWRfaW50ZWdlchgEIAEoBEgAEhAKBm51bWJlchgFIAEoAUgAEhAKBmJpbmFyeRgGIAEoDEgAEiwKBGxpc3QYByABKAsyHC5raXQuc2FuZGJveC52MS5NZXRhZGF0YUxpc3RIABIwCgZvYmplY3QYCCABKAsyHi5raXQuc2FuZGJveC52MS5NZXRhZGF0YU9iamVjdEgAQgcKBXZhbHVlIj0KDE1ldGFkYXRhTGlzdBItCgZ2YWx1ZXMYASADKAsyHS5raXQuc2FuZGJveC52MS5NZXRhZGF0YVZhbHVlIpoBCg5NZXRhZGF0YU9iamVjdBI6CgZ2YWx1ZXMYASADKAsyKi5raXQuc2FuZGJveC52MS5NZXRhZGF0YU9iamVjdC5WYWx1ZXNFbnRyeRpMCgtWYWx1ZXNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5raXQuc2FuZGJveC52MS5NZXRhZGF0YVZhbHVlOgI4ASrYAQoMU25hcHNob3RLaW5kEhkKFVNOQVBTSE9UX0tJTkRfREVGQVVMVBAAEhwKGFNOQVBTSE9UX0tJTkRfRklMRVNZU1RFTRABEhgKFFNOQVBTSE9UX0tJTkRfTUVNT1JZEAISFwoTU05BUFNIT1RfS0lORF9JTUFHRRADEhkKFVNOQVBTSE9UX0tJTkRfTUFDSElORRAEEhwKGFNOQVBTSE9UX0tJTkRfQ0hFQ0tQT0lOVBAFEiMKH1NOQVBTSE9UX0tJTkRfUFJPVklERVJfU1BFQ0lGSUMQBirDAQoNSXNvbGF0aW9uS2luZBIaChZJU09MQVRJT05fS0lORF9ERUZBVUxUEAASHAoYSVNPTEFUSU9OX0tJTkRfQ09OVEFJTkVSEAESGgoWSVNPTEFUSU9OX0tJTkRfTUlDUk9WTRACEhsKF0lTT0xBVElPTl9LSU5EX0xJTlVYX1ZNEAMSGQoVSVNPTEFUSU9OX0tJTkRfV09SS0VSEAQSJAogSVNPTEFUSU9OX0tJTkRfUFJPVklERVJfU1BFQ0lGSUMQBSp3CgpFZ3Jlc3NNb2RlEhcKE0VHUkVTU19NT0RFX0RFRkFVTFQQABIZChVFR1JFU1NfTU9ERV9BTExPV19BTEwQARIZChVFR1JFU1NfTU9ERV9CTE9DS19BTEwQAhIaChZFR1JFU1NfTU9ERV9SRVNUUklDVEVEEAMqewoNUG9ydFRyYW5zcG9ydBIaChZQT1JUX1RSQU5TUE9SVF9ERUZBVUxUEAASFgoSUE9SVF9UUkFOU1BPUlRfVExTEAESHAoYUE9SVF9UUkFOU1BPUlRfSFRUUDJfVExTEAISGAoUUE9SVF9UUkFOU1BPUlRfUExBSU4QAyp6Cg9TZWNyZXRJbmplY3Rpb24SHAoYU0VDUkVUX0lOSkVDVElPTl9ERUZBVUxUEAASIAocU0VDUkVUX0lOSkVDVElPTl9FTlZJUk9OTUVOVBABEicKI1NFQ1JFVF9JTkpFQ1RJT05fRUdSRVNTX1BMQUNFSE9MREVSEAIqVgoKUG9saWN5TW9kZRIXChNQT0xJQ1lfTU9ERV9ERUZBVUxUEAASGAoUUE9MSUNZX01PREVfRElTQUJMRUQQARIVChFQT0xJQ1lfTU9ERV9BRlRFUhACKp0BCg1XYWl0Q29uZGl0aW9uEhoKFldBSVRfQ09ORElUSU9OX0RFRkFVTFQQABIcChhXQUlUX0NPTkRJVElPTl9TVUJNSVRURUQQARIcChhXQUlUX0NPTkRJVElPTl9TQ0hFRFVMRUQQAhIaChZXQUlUX0NPTkRJVElPTl9TVEFSVEVEEAMSGAoUV0FJVF9DT05ESVRJT05fUkVBRFkQBCppCghGaWxlVHlwZRIZChVGSUxFX1RZUEVfVU5TUEVDSUZJRUQQABISCg5GSUxFX1RZUEVfRklMRRABEhcKE0ZJTEVfVFlQRV9ESVJFQ1RPUlkQAhIVChFGSUxFX1RZUEVfU1lNTElOSxADKqwDCgxTYW5kYm94U3RhdGUSGQoVU0FOREJPWF9TVEFURV9VTktOT1dOEAASGAoUU0FOREJPWF9TVEFURV9RVUVVRUQQARIaChZTQU5EQk9YX1NUQVRFX1NUQVJUSU5HEAISGQoVU0FOREJPWF9TVEFURV9SVU5OSU5HEAMSGgoWU0FOREJPWF9TVEFURV9TVE9QUElORxAEEhkKFVNBTkRCT1hfU1RBVEVfU1RPUFBFRBAFEhkKFVNBTkRCT1hfU1RBVEVfUEFVU0lORxAGEhgKFFNBTkRCT1hfU1RBVEVfUEFVU0VEEAcSGwoXU0FOREJPWF9TVEFURV9BUkNISVZJTkcQCBIaChZTQU5EQk9YX1NUQVRFX0FSQ0hJVkVEEAkSHQoZU0FOREJPWF9TVEFURV9URVJNSU5BVElORxAKEhwKGFNBTkRCT1hfU1RBVEVfVEVSTUlOQVRFRBALEhoKFlNBTkRCT1hfU1RBVEVfREVMRVRJTkcQDBIZChVTQU5EQk9YX1NUQVRFX0RFTEVURUQQDRIXChNTQU5EQk9YX1NUQVRFX0VSUk9SEA4qnwIKE0xpZmVjeWNsZUNhcGFiaWxpdHkSIAocTElGRUNZQ0xFX0NBUEFCSUxJVFlfVU5LTk9XThAAEiAKHExJRkVDWUNMRV9DQVBBQklMSVRZX1JFRlJFU0gQARIeChpMSUZFQ1lDTEVfQ0FQQUJJTElUWV9TVEFSVBACEh0KGUxJRkVDWUNMRV9DQVBBQklMSVRZX1NUT1AQAxIeChpMSUZFQ1lDTEVfQ0FQQUJJTElUWV9QQVVTRRAEEiIKHkxJRkVDWUNMRV9DQVBBQklMSVRZX1RFUk1JTkFURRAFEiAKHExJRkVDWUNMRV9DQVBBQklMSVRZX0FSQ0hJVkUQBhIfChtMSUZFQ1lDTEVfQ0FQQUJJTElUWV9ERUxFVEUQByreAwoJRXJyb3JLaW5kEhYKEkVSUk9SX0tJTkRfVU5LTk9XThAAEh8KG0VSUk9SX0tJTkRfSU5WQUxJRF9BUkdVTUVOVBABEhoKFkVSUk9SX0tJTkRfVU5TVVBQT1JURUQQAhIdChlFUlJPUl9LSU5EX0FVVEhFTlRJQ0FUSU9OEAMSIAocRVJST1JfS0lORF9QRVJNSVNTSU9OX0RFTklFRBAEEhgKFEVSUk9SX0tJTkRfTk9UX0ZPVU5EEAUSHQoZRVJST1JfS0lORF9BTFJFQURZX0VYSVNUUxAGEhcKE0VSUk9SX0tJTkRfQ09ORkxJQ1QQBxIbChdFUlJPUl9LSU5EX1JBVEVfTElNSVRFRBAIEiEKHUVSUk9SX0tJTkRfUkVTT1VSQ0VfRVhIQVVTVEVEEAkSFgoSRVJST1JfS0lORF9USU1FT1VUEAoSFwoTRVJST1JfS0lORF9DQU5DRUxFRBALEhoKFkVSUk9SX0tJTkRfVU5BVkFJTEFCTEUQDBIfChtFUlJPUl9LSU5EX0lOVkFMSURfUkVTUE9OU0UQDRIXChNFUlJPUl9LSU5EX0lOVEVSTkFMEA4SIgoeRVJST1JfS0lORF9GQUlMRURfUFJFQ09ORElUSU9OEA9CUFpOZ2l0aHViLmNvbS9zYW5kYm94LWtpdC9raXQvc2Rrcy9nby9pbnRlcm5hbC9jb250cmFjdHMva2l0L3NhbmRib3gvdjE7c2FuZGJveHYxYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_duration, file_google_protobuf_timestamp]); + fileDesc("ChxraXQvc2FuZGJveC92MS9zYW5kYm94LnByb3RvEg5raXQuc2FuZGJveC52MSLdBgoNQ3JlYXRlT3B0aW9ucxIRCgRuYW1lGAEgASgJSACIAQESLQoGc291cmNlGAIgASgLMh0ua2l0LnNhbmRib3gudjEuU2FuZGJveFNvdXJjZRIuCgdydW50aW1lGAMgASgLMh0ua2l0LnNhbmRib3gudjEuUnVudGltZUNvbmZpZxIyCglpc29sYXRpb24YBCABKAsyHy5raXQuc2FuZGJveC52MS5Jc29sYXRpb25Db25maWcSLAoJcmVzb3VyY2VzGAUgASgLMhkua2l0LnNhbmRib3gudjEuUmVzb3VyY2VzEiwKCXBsYWNlbWVudBgGIAEoCzIZLmtpdC5zYW5kYm94LnYxLlBsYWNlbWVudBJDCgtlbnZpcm9ubWVudBgHIAMoCzIuLmtpdC5zYW5kYm94LnYxLkNyZWF0ZU9wdGlvbnMuRW52aXJvbm1lbnRFbnRyeRI5CgZsYWJlbHMYCCADKAsyKS5raXQuc2FuZGJveC52MS5DcmVhdGVPcHRpb25zLkxhYmVsc0VudHJ5Ei4KB25ldHdvcmsYCSABKAsyHS5raXQuc2FuZGJveC52MS5OZXR3b3JrQ29uZmlnEjAKCHNlY3VyaXR5GAogASgLMh4ua2l0LnNhbmRib3gudjEuU2VjdXJpdHlDb25maWcSMAoIbGlmZXRpbWUYCyABKAsyHi5raXQuc2FuZGJveC52MS5MaWZldGltZVBvbGljeRJDCgxwcm92aXNpb25pbmcYDCABKAsyIy5raXQuc2FuZGJveC52MS5Qcm92aXNpb25pbmdPcHRpb25zUghjcmVhdGlvbhIxCglyZWFkaW5lc3MYDSABKAsyHi5raXQuc2FuZGJveC52MS5SZWFkaW5lc3NQcm9iZRI6Cg1vYnNlcnZhYmlsaXR5GA4gASgLMiMua2l0LnNhbmRib3gudjEuT2JzZXJ2YWJpbGl0eUNvbmZpZxoyChBFbnZpcm9ubWVudEVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIHCgVfbmFtZUoECA8QEFIQcHJvdmlkZXJfb3B0aW9ucyLxAQoNU2FuZGJveFNvdXJjZRJBChBwcm92aWRlcl9kZWZhdWx0GAEgASgLMiUua2l0LnNhbmRib3gudjEuUHJvdmlkZXJEZWZhdWx0U291cmNlSAASLAoFaW1hZ2UYAiABKAsyGy5raXQuc2FuZGJveC52MS5JbWFnZVNvdXJjZUgAEjIKCHNuYXBzaG90GAMgASgLMh4ua2l0LnNhbmRib3gudjEuU25hcHNob3RTb3VyY2VIABIzCgl3YXJtX3Bvb2wYBCABKAsyHi5raXQuc2FuZGJveC52MS5XYXJtUG9vbFNvdXJjZUgAQgYKBGtpbmQiFwoVUHJvdmlkZXJEZWZhdWx0U291cmNlIo4DCgtJbWFnZVNvdXJjZRL+AgoJcmVmZXJlbmNlGAEgASgJQuoCukjmAroBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJ7oBrwIKD2ltYWdlX3JlZmVyZW5jZRIjbXVzdCBiZSBhIGNvbnRhaW5lciBpbWFnZSByZWZlcmVuY2Ua9gF0aGlzLnNpemUoKSA8PSA1MTIgJiYgdGhpcy5tYXRjaGVzKCdeKD86W0EtWmEtejAtOV0oPzpbQS1aYS16MC05Li1dKltBLVphLXowLTldKT8oPzo6WzAtOV0rKT8vKT9bYS16MC05XSsoPzooPzpbLl9dfF9ffC0rKVthLXowLTldKykqKD86L1thLXowLTldKyg/Oig/OlsuX118X198LSspW2EtejAtOV0rKSopKig/OjpbQS1aYS16MC05X11bQS1aYS16MC05Xy4tXXswLDEyN30pPyg/OkBzaGEyNTY6W2EtZkEtRjAtOV17NjR9KT8kJykivwEKDlNuYXBzaG90U291cmNlEkkKCXJlZmVyZW5jZRgBIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnEjQKBGtpbmQYAiABKA4yHC5raXQuc2FuZGJveC52MS5TbmFwc2hvdEtpbmRCCLpIBYIBAhABEhoKDXByb3ZpZGVyX2tpbmQYAyABKAlIAIgBAUIQCg5fcHJvdmlkZXJfa2luZCJbCg5XYXJtUG9vbFNvdXJjZRJJCglyZWZlcmVuY2UYASABKAlCNrpIM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJyLyAwoNUnVudGltZUNvbmZpZxJNCghsYW5ndWFnZRgBIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSACIAQESTAoHdmVyc2lvbhgCIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSAGIAQESSQoEdXNlchgDIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSAKIAQESVgoRd29ya2luZ19kaXJlY3RvcnkYBCABKAlCNrpIM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJ0gDiAEBEk8KCmVudHJ5cG9pbnQYBSADKAlCO7pIOJIBNSIzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnEhAKA3B0eRgGIAEoCEgEiAEBQgsKCV9sYW5ndWFnZUIKCghfdmVyc2lvbkIHCgVfdXNlckIUChJfd29ya2luZ19kaXJlY3RvcnlCBgoEX3B0eSK0AQoPSXNvbGF0aW9uQ29uZmlnEjUKBGtpbmQYASABKA4yHS5raXQuc2FuZGJveC52MS5Jc29sYXRpb25LaW5kQgi6SAWCAQIQARIaCg1wcm92aWRlcl9raW5kGAIgASgJSACIAQESIgoVbmVzdGVkX3ZpcnR1YWxpemF0aW9uGAMgASgISAGIAQFCEAoOX3Byb3ZpZGVyX2tpbmRCGAoWX25lc3RlZF92aXJ0dWFsaXphdGlvbiKTCwoJUmVzb3VyY2VzEpABCgljcHVfY29yZXMYASABKAFCeLpIdboBOAoGZmluaXRlEg5tdXN0IGJlIGZpbml0ZRoeIXRoaXMuaXNOYW4oKSAmJiAhdGhpcy5pc0luZigpugE3ChFleGNsdXNpdmVfbWluaW11bRIWbXVzdCBiZSBncmVhdGVyIHRoYW4gMBoKdGhpcyA+IDAuMEgAiAEBEokBCg9jcHVfbGltaXRfY29yZXMYAiABKAFCa7pIaLoBOAoGZmluaXRlEg5tdXN0IGJlIGZpbml0ZRoeIXRoaXMuaXNOYW4oKSAmJiAhdGhpcy5pc0luZigpugEqCgdtaW5pbXVtEhJtdXN0IGJlIGF0IGxlYXN0IDAaC3RoaXMgPj0gMC4wSAGIAQESVQoKbWVtb3J5X21pYhgDIAEoBEI8ukg5ugE2ChFleGNsdXNpdmVfbWluaW11bRIWbXVzdCBiZSBncmVhdGVyIHRoYW4gMBoJdGhpcyA+IDB1SAKIAQESHQoQbWVtb3J5X2xpbWl0X21pYhgEIAEoBEgDiAEBElMKCGRpc2tfbWliGAUgASgEQjy6SDm6ATYKEWV4Y2x1c2l2ZV9taW5pbXVtEhZtdXN0IGJlIGdyZWF0ZXIgdGhhbiAwGgl0aGlzID4gMHVIBIgBATrIBrpIxAYazwEKCWNwdV9saW1pdBJFY3B1X2xpbWl0X2NvcmVzIG11c3QgYmUgYXQgbGVhc3QgY3B1X2NvcmVzIHdoZW4gdGhlIGxpbWl0IGlzIHBvc2l0aXZlGnshaGFzKHRoaXMuY3B1X2xpbWl0X2NvcmVzKSB8fCB0aGlzLmNwdV9saW1pdF9jb3JlcyA8PSAwLjAgfHwgIWhhcyh0aGlzLmNwdV9jb3JlcykgfHwgdGhpcy5jcHVfbGltaXRfY29yZXMgPj0gdGhpcy5jcHVfY29yZXMa2AEKDG1lbW9yeV9saW1pdBJHbWVtb3J5X2xpbWl0X21pYiBtdXN0IGJlIGF0IGxlYXN0IG1lbW9yeV9taWIgd2hlbiB0aGUgbGltaXQgaXMgcG9zaXRpdmUafyFoYXModGhpcy5tZW1vcnlfbGltaXRfbWliKSB8fCB0aGlzLm1lbW9yeV9saW1pdF9taWIgPT0gMHUgfHwgIWhhcyh0aGlzLm1lbW9yeV9taWIpIHx8IHRoaXMubWVtb3J5X2xpbWl0X21pYiA+PSB0aGlzLm1lbW9yeV9taWIaxQEKFWNwdV9yZXF1aXJlc19wb3NpdGl2ZRJAYSBwb3NpdGl2ZSBjcHVfbGltaXRfY29yZXMgcmVxdWlyZXMgYSBwb3NpdGl2ZSBjcHVfY29yZXMgcmVxdWVzdBpqIWhhcyh0aGlzLmNwdV9saW1pdF9jb3JlcykgfHwgdGhpcy5jcHVfbGltaXRfY29yZXMgPD0gMC4wIHx8IChoYXModGhpcy5jcHVfY29yZXMpICYmIHRoaXMuY3B1X2NvcmVzID4gMC4wKRrMAQoYbWVtb3J5X3JlcXVpcmVzX3Bvc2l0aXZlEkJhIHBvc2l0aXZlIG1lbW9yeV9saW1pdF9taWIgcmVxdWlyZXMgYSBwb3NpdGl2ZSBtZW1vcnlfbWliIHJlcXVlc3QabCFoYXModGhpcy5tZW1vcnlfbGltaXRfbWliKSB8fCB0aGlzLm1lbW9yeV9saW1pdF9taWIgPT0gMHUgfHwgKGhhcyh0aGlzLm1lbW9yeV9taWIpICYmIHRoaXMubWVtb3J5X21pYiA+IDB1KUIMCgpfY3B1X2NvcmVzQhIKEF9jcHVfbGltaXRfY29yZXNCDQoLX21lbW9yeV9taWJCEwoRX21lbW9yeV9saW1pdF9taWJCCwoJX2Rpc2tfbWliIpcBCglQbGFjZW1lbnQSEgoFY2xvdWQYASABKAlIAIgBARJMCgdyZWdpb25zGAIgAygJQju6SDiSATUiM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJxITCgZ0YXJnZXQYAyABKAlIAYgBAUIICgZfY2xvdWRCCQoHX3RhcmdldCIiCg9TdHJpbmdBbGxvd2xpc3QSDwoHZW50cmllcxgBIAMoCSKXCgoNTmV0d29ya0NvbmZpZxI0CgZlZ3Jlc3MYASABKA4yGi5raXQuc2FuZGJveC52MS5FZ3Jlc3NNb2RlQgi6SAWCAQIQARI3Cg5vdXRib3VuZF9jaWRycxgCIAEoCzIfLmtpdC5zYW5kYm94LnYxLlN0cmluZ0FsbG93bGlzdBI5ChBvdXRib3VuZF9kb21haW5zGAMgASgLMh8ua2l0LnNhbmRib3gudjEuU3RyaW5nQWxsb3dsaXN0EjYKDWluYm91bmRfY2lkcnMYBCABKAsyHy5raXQuc2FuZGJveC52MS5TdHJpbmdBbGxvd2xpc3QSHAoPcHJpdmF0ZV9uZXR3b3JrGAUgASgISACIAQESUwoObGlua2VkX3NhbmRib3gYBiABKAlCNrpIM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJ0gBiAEBElQKD3Byb3h5X3JlZmVyZW5jZRgHIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSAKIAQESHwoSb3V0Ym91bmRfcHJveHlfdXJsGAggASgJSAOIAQESGgoNY3VzdG9tX2RvbWFpbhgJIAEoCUgEiAEBEioKBXBvcnRzGAogAygLMhsua2l0LnNhbmRib3gudjEuUG9ydEJpbmRpbmc6jQW6SIkFGqoBCh1yZXN0cmljdGVkX3JlcXVpcmVzX2FsbG93bGlzdBI9cmVzdHJpY3RlZCBlZ3Jlc3MgcmVxdWlyZXMgb3V0Ym91bmRfY2lkcnMgb3Igb3V0Ym91bmRfZG9tYWlucxpKdGhpcy5lZ3Jlc3MgIT0gMyB8fCBoYXModGhpcy5vdXRib3VuZF9jaWRycykgfHwgaGFzKHRoaXMub3V0Ym91bmRfZG9tYWlucykaugIKGWJsb2NrX2FsbF9mb3JiaWRzX25ldHdvcmsSQGJsb2NrLWFsbCBlZ3Jlc3MgZm9yYmlkcyBuZXR3b3JraW5nIGFsbG93bGlzdHMsIGxpbmtzLCBhbmQgcG9ydHMa2gF0aGlzLmVncmVzcyAhPSAyIHx8ICghaGFzKHRoaXMub3V0Ym91bmRfY2lkcnMpICYmICFoYXModGhpcy5vdXRib3VuZF9kb21haW5zKSAmJiAhaGFzKHRoaXMuaW5ib3VuZF9jaWRycykgJiYgIWhhcyh0aGlzLmxpbmtlZF9zYW5kYm94KSAmJiB0aGlzLnBvcnRzLnNpemUoKSA9PSAwICYmICghaGFzKHRoaXMucHJpdmF0ZV9uZXR3b3JrKSB8fCAhdGhpcy5wcml2YXRlX25ldHdvcmspKRqcAQocYWxsb3dfYWxsX2ZvcmJpZHNfYWxsb3dsaXN0cxIsYWxsb3ctYWxsIGVncmVzcyBmb3JiaWRzIG91dGJvdW5kIGFsbG93bGlzdHMaTnRoaXMuZWdyZXNzICE9IDEgfHwgKCFoYXModGhpcy5vdXRib3VuZF9jaWRycykgJiYgIWhhcyh0aGlzLm91dGJvdW5kX2RvbWFpbnMpKUISChBfcHJpdmF0ZV9uZXR3b3JrQhEKD19saW5rZWRfc2FuZGJveEISChBfcHJveHlfcmVmZXJlbmNlQhUKE19vdXRib3VuZF9wcm94eV91cmxCEAoOX2N1c3RvbV9kb21haW4ijwEKC1BvcnRCaW5kaW5nEkQKBHBvcnQYASABKA1CNrpIM7oBMAoHbWF4aW11bRIVbXVzdCBiZSBhdCBtb3N0IDY1NTM1Gg50aGlzIDw9IDY1NTM1dRI6Cgl0cmFuc3BvcnQYAiABKA4yHS5raXQuc2FuZGJveC52MS5Qb3J0VHJhbnNwb3J0Qgi6SAWCAQIQASKmAQoOU2VjdXJpdHlDb25maWcSGgoNcHVibGljX2FjY2VzcxgBIAEoCEgAiAEBEh4KEXdvcmtsb2FkX2lkZW50aXR5GAIgASgISAGIAQESMAoHc2VjcmV0cxgDIAMoCzIfLmtpdC5zYW5kYm94LnYxLlNlY3JldFJlZmVyZW5jZUIQCg5fcHVibGljX2FjY2Vzc0IUChJfd29ya2xvYWRfaWRlbnRpdHkijgIKD1NlY3JldFJlZmVyZW5jZRJJCglyZWZlcmVuY2UYASABKAlCNrpIM7oBMAoIbm9uYmxhbmsSEW11c3Qgbm90IGJlIGJsYW5rGhF0aGlzLnRyaW0oKSAhPSAnJxJZChRlbnZpcm9ubWVudF92YXJpYWJsZRgCIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSACIAQESPAoJaW5qZWN0aW9uGAMgASgOMh8ua2l0LnNhbmRib3gudjEuU2VjcmV0SW5qZWN0aW9uQgi6SAWCAQIQAUIXChVfZW52aXJvbm1lbnRfdmFyaWFibGUiigMKDkxpZmV0aW1lUG9saWN5Ej0KEG1heGltdW1fbGlmZXRpbWUYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CCLpIBaoBAjIAEhYKCWVwaGVtZXJhbBgCIAEoCEgAiAEBEjcKDmlkbGVfdGVybWluYXRlGAMgASgLMh8ua2l0LnNhbmRib3gudjEuQXV0b21hdGljQWN0aW9uEjIKCWlkbGVfc3RvcBgEIAEoCzIfLmtpdC5zYW5kYm94LnYxLkF1dG9tYXRpY0FjdGlvbhIzCgppZGxlX3BhdXNlGAUgASgLMh8ua2l0LnNhbmRib3gudjEuQXV0b21hdGljQWN0aW9uEjgKD3N0b3BwZWRfYXJjaGl2ZRgGIAEoCzIfLmtpdC5zYW5kYm94LnYxLkF1dG9tYXRpY0FjdGlvbhI3Cg5zdG9wcGVkX2RlbGV0ZRgHIAEoCzIfLmtpdC5zYW5kYm94LnYxLkF1dG9tYXRpY0FjdGlvbkIMCgpfZXBoZW1lcmFsIr0CCg9BdXRvbWF0aWNBY3Rpb24SMgoEbW9kZRgBIAEoDjIaLmtpdC5zYW5kYm94LnYxLlBvbGljeU1vZGVCCLpIBYIBAhABEjIKBWFmdGVyGAIgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQgi6SAWqAQIyADrBAbpIvQEaVwoXYWZ0ZXJfcmVxdWlyZXNfZHVyYXRpb24SGW1vZGUgYWZ0ZXIgcmVxdWlyZXMgYWZ0ZXIaIXRoaXMubW9kZSAhPSAyIHx8IGhhcyh0aGlzLmFmdGVyKRpiChZhZnRlcl9mb3JiaWRzX2R1cmF0aW9uEiRhZnRlciBpcyBvbmx5IHNldCB3aGVuIG1vZGUgaXMgYWZ0ZXIaInRoaXMubW9kZSA9PSAyIHx8ICFoYXModGhpcy5hZnRlcikiwgEKE1Byb3Zpc2lvbmluZ09wdGlvbnMSNAoHdGltZW91dBgBIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkIIukgFqgECMgASOgoNcXVldWVfdGltZW91dBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkIIukgFqgECMgASOQoId2FpdF9mb3IYAyABKA4yHS5raXQuc2FuZGJveC52MS5XYWl0Q29uZGl0aW9uQgi6SAWCAQIQASLJAQoOUmVhZGluZXNzUHJvYmUSdgoIdGNwX3BvcnQYASABKA1CYrpIX7oBKQoHbWluaW11bRISbXVzdCBiZSBhdCBsZWFzdCAxGgp0aGlzID49IDF1ugEwCgdtYXhpbXVtEhVtdXN0IGJlIGF0IG1vc3QgNjU1MzUaDnRoaXMgPD0gNjU1MzV1SAASLwoHY29tbWFuZBgCIAEoCzIcLmtpdC5zYW5kYm94LnYxLkNvbW1hbmRQcm9iZUgAQg4KBWNoZWNrEgW6SAIIASJlCgxDb21tYW5kUHJvYmUSVQoEYXJndhgBIAMoCUJHukhEugFBCgltaW5faXRlbXMSIm11c3QgY29udGFpbiBhdCBsZWFzdCBvbmUgYXJndW1lbnQaEHRoaXMuc2l6ZSgpID49IDEipwEKE09ic2VydmFiaWxpdHlDb25maWcSFAoHdmVyYm9zZRgBIAEoCEgAiAEBElcKEnRlbGVtZXRyeV9lbmRwb2ludBgCIAEoCUI2ukgzugEwCghub25ibGFuaxIRbXVzdCBub3QgYmUgYmxhbmsaEXRoaXMudHJpbSgpICE9ICcnSAGIAQFCCgoIX3ZlcmJvc2VCFQoTX3RlbGVtZXRyeV9lbmRwb2ludCI8CgxDcmVhdGVSZXN1bHQSLAoHc2FuZGJveBgBIAEoCzIbLmtpdC5zYW5kYm94LnYxLlNhbmRib3hJbmZvIjoKDFN0YXJ0T3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIlcKC1N0b3BPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24SEgoFZm9yY2UYAiABKAhIAIgBAUIICgZfZm9yY2UiOgoMUGF1c2VPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iWgoQVGVybWluYXRlT3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEhEKBHdhaXQYAiABKAhIAIgBAUIHCgVfd2FpdCI8Cg5BcmNoaXZlT3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIlcKDURlbGV0ZU9wdGlvbnMSKgoHdGltZW91dBgBIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhIRCgR3YWl0GAIgASgISACIAQFCBwoFX3dhaXQiMgoKRXhpdFJlc3VsdBIWCglleGl0X2NvZGUYASABKAVIAIgBAUIMCgpfZXhpdF9jb2RlIskCCghGaWxlSW5mbxIMCgRuYW1lGAEgASgJEgwKBHBhdGgYAiABKAkSMAoEdHlwZRgDIAEoDjIYLmtpdC5zYW5kYm94LnYxLkZpbGVUeXBlQgi6SAWCAQIQARIMCgRzaXplGAQgASgDEhEKBG1vZGUYBSABKAlIAIgBARIYCgtwZXJtaXNzaW9ucxgGIAEoCUgBiAEBEhIKBW93bmVyGAcgASgJSAKIAQESEgoFZ3JvdXAYCCABKAlIA4gBARIvCgttb2RpZmllZF9hdBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoOc3ltbGlua190YXJnZXQYCiABKAlIBIgBAUIHCgVfbW9kZUIOCgxfcGVybWlzc2lvbnNCCAoGX293bmVyQggKBl9ncm91cEIRCg9fc3ltbGlua190YXJnZXQiPQoPRmlsZVJlYWRPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iPgoQRmlsZVdyaXRlT3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIj0KD0ZpbGVDb3B5T3B0aW9ucxIqCgd0aW1lb3V0GAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uImQKFE1ha2VEaXJlY3RvcnlPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24SFAoHcGFyZW50cxgCIAEoCEgAiAEBQgoKCF9wYXJlbnRzIkIKFExpc3REaXJlY3RvcnlPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iOQoLU3RhdE9wdGlvbnMSKgoHdGltZW91dBgBIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbiJhCg1SZW1vdmVPcHRpb25zEioKB3RpbWVvdXQYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24SFgoJcmVjdXJzaXZlGAIgASgISACIAQFCDAoKX3JlY3Vyc2l2ZSKSAgoLRXhlY09wdGlvbnMSDAoEYXJndhgBIAMoCRISCgVzaGVsbBgCIAEoCUgAiAEBEh4KEXdvcmtpbmdfZGlyZWN0b3J5GAMgASgJSAGIAQESQQoLZW52aXJvbm1lbnQYBCADKAsyLC5raXQuc2FuZGJveC52MS5FeGVjT3B0aW9ucy5FbnZpcm9ubWVudEVudHJ5EioKB3RpbWVvdXQYBSABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24aMgoQRW52aXJvbm1lbnRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQggKBl9zaGVsbEIUChJfd29ya2luZ19kaXJlY3RvcnkiPwoKRXhlY1Jlc3VsdBIRCglleGl0X2NvZGUYASABKAUSDgoGc3Rkb3V0GAIgASgMEg4KBnN0ZGVychgDIAEoDCKsBAoLU2FuZGJveEluZm8SCgoCaWQYASABKAkSEAoIcHJvdmlkZXIYAiABKAkSEQoEbmFtZRgDIAEoCUgAiAEBEhsKDnByb3ZpZGVyX3N0YXRlGAQgASgJSAGIAQESEwoGcmVnaW9uGAUgASgJSAKIAQESMgoJaXNvbGF0aW9uGAYgASgLMh8ua2l0LnNhbmRib3gudjEuSXNvbGF0aW9uQ29uZmlnEiwKCXJlc291cmNlcxgHIAEoCzIZLmtpdC5zYW5kYm94LnYxLlJlc291cmNlcxI3CgZsYWJlbHMYCCADKAsyJy5raXQuc2FuZGJveC52MS5TYW5kYm94SW5mby5MYWJlbHNFbnRyeRIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBI5ChFwcm92aWRlcl9tZXRhZGF0YRgKIAEoCzIeLmtpdC5zYW5kYm94LnYxLk1ldGFkYXRhT2JqZWN0EisKBXN0YXRlGAwgASgOMhwua2l0LnNhbmRib3gudjEuU2FuZGJveFN0YXRlEhUKCHRlcm1pbmFsGA0gASgISAOIAQEaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIHCgVfbmFtZUIRCg9fcHJvdmlkZXJfc3RhdGVCCQoHX3JlZ2lvbkILCglfdGVybWluYWxKBAgLEAxSB29yaWdpbnMinwIKCUVycm9ySW5mbxInCgRraW5kGAEgASgOMhkua2l0LnNhbmRib3gudjEuRXJyb3JLaW5kEhAKCHByb3ZpZGVyGAIgASgJEhEKCW9wZXJhdGlvbhgDIAEoCRINCgVmaWVsZBgEIAEoCRIPCgdtZXNzYWdlGAUgASgJEhgKC3N0YXR1c19jb2RlGAYgASgNSACIAQESGgoNcHJvdmlkZXJfY29kZRgHIAEoCUgBiAEBEhwKD3Byb3ZpZGVyX3NvdXJjZRgIIAEoCUgCiAEBEhEKBHJ1bGUYCSABKAlIA4gBAUIOCgxfc3RhdHVzX2NvZGVCEAoOX3Byb3ZpZGVyX2NvZGVCEgoQX3Byb3ZpZGVyX3NvdXJjZUIHCgVfcnVsZSL1AQoNTWV0YWRhdGFWYWx1ZRIRCgdib29sZWFuGAEgASgISAASDgoEdGV4dBgCIAEoCUgAEhgKDnNpZ25lZF9pbnRlZ2VyGAMgASgDSAASGgoQdW5zaWduZWRfaW50ZWdlchgEIAEoBEgAEhAKBm51bWJlchgFIAEoAUgAEhAKBmJpbmFyeRgGIAEoDEgAEiwKBGxpc3QYByABKAsyHC5raXQuc2FuZGJveC52MS5NZXRhZGF0YUxpc3RIABIwCgZvYmplY3QYCCABKAsyHi5raXQuc2FuZGJveC52MS5NZXRhZGF0YU9iamVjdEgAQgcKBXZhbHVlIj0KDE1ldGFkYXRhTGlzdBItCgZ2YWx1ZXMYASADKAsyHS5raXQuc2FuZGJveC52MS5NZXRhZGF0YVZhbHVlIpoBCg5NZXRhZGF0YU9iamVjdBI6CgZ2YWx1ZXMYASADKAsyKi5raXQuc2FuZGJveC52MS5NZXRhZGF0YU9iamVjdC5WYWx1ZXNFbnRyeRpMCgtWYWx1ZXNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5raXQuc2FuZGJveC52MS5NZXRhZGF0YVZhbHVlOgI4ASrYAQoMU25hcHNob3RLaW5kEhkKFVNOQVBTSE9UX0tJTkRfREVGQVVMVBAAEhwKGFNOQVBTSE9UX0tJTkRfRklMRVNZU1RFTRABEhgKFFNOQVBTSE9UX0tJTkRfTUVNT1JZEAISFwoTU05BUFNIT1RfS0lORF9JTUFHRRADEhkKFVNOQVBTSE9UX0tJTkRfTUFDSElORRAEEhwKGFNOQVBTSE9UX0tJTkRfQ0hFQ0tQT0lOVBAFEiMKH1NOQVBTSE9UX0tJTkRfUFJPVklERVJfU1BFQ0lGSUMQBirDAQoNSXNvbGF0aW9uS2luZBIaChZJU09MQVRJT05fS0lORF9ERUZBVUxUEAASHAoYSVNPTEFUSU9OX0tJTkRfQ09OVEFJTkVSEAESGgoWSVNPTEFUSU9OX0tJTkRfTUlDUk9WTRACEhsKF0lTT0xBVElPTl9LSU5EX0xJTlVYX1ZNEAMSGQoVSVNPTEFUSU9OX0tJTkRfV09SS0VSEAQSJAogSVNPTEFUSU9OX0tJTkRfUFJPVklERVJfU1BFQ0lGSUMQBSp3CgpFZ3Jlc3NNb2RlEhcKE0VHUkVTU19NT0RFX0RFRkFVTFQQABIZChVFR1JFU1NfTU9ERV9BTExPV19BTEwQARIZChVFR1JFU1NfTU9ERV9CTE9DS19BTEwQAhIaChZFR1JFU1NfTU9ERV9SRVNUUklDVEVEEAMqewoNUG9ydFRyYW5zcG9ydBIaChZQT1JUX1RSQU5TUE9SVF9ERUZBVUxUEAASFgoSUE9SVF9UUkFOU1BPUlRfVExTEAESHAoYUE9SVF9UUkFOU1BPUlRfSFRUUDJfVExTEAISGAoUUE9SVF9UUkFOU1BPUlRfUExBSU4QAyp6Cg9TZWNyZXRJbmplY3Rpb24SHAoYU0VDUkVUX0lOSkVDVElPTl9ERUZBVUxUEAASIAocU0VDUkVUX0lOSkVDVElPTl9FTlZJUk9OTUVOVBABEicKI1NFQ1JFVF9JTkpFQ1RJT05fRUdSRVNTX1BMQUNFSE9MREVSEAIqVgoKUG9saWN5TW9kZRIXChNQT0xJQ1lfTU9ERV9ERUZBVUxUEAASGAoUUE9MSUNZX01PREVfRElTQUJMRUQQARIVChFQT0xJQ1lfTU9ERV9BRlRFUhACKp0BCg1XYWl0Q29uZGl0aW9uEhoKFldBSVRfQ09ORElUSU9OX0RFRkFVTFQQABIcChhXQUlUX0NPTkRJVElPTl9TVUJNSVRURUQQARIcChhXQUlUX0NPTkRJVElPTl9TQ0hFRFVMRUQQAhIaChZXQUlUX0NPTkRJVElPTl9TVEFSVEVEEAMSGAoUV0FJVF9DT05ESVRJT05fUkVBRFkQBCppCghGaWxlVHlwZRIZChVGSUxFX1RZUEVfVU5TUEVDSUZJRUQQABISCg5GSUxFX1RZUEVfRklMRRABEhcKE0ZJTEVfVFlQRV9ESVJFQ1RPUlkQAhIVChFGSUxFX1RZUEVfU1lNTElOSxADKqwDCgxTYW5kYm94U3RhdGUSGQoVU0FOREJPWF9TVEFURV9VTktOT1dOEAASGAoUU0FOREJPWF9TVEFURV9RVUVVRUQQARIaChZTQU5EQk9YX1NUQVRFX1NUQVJUSU5HEAISGQoVU0FOREJPWF9TVEFURV9SVU5OSU5HEAMSGgoWU0FOREJPWF9TVEFURV9TVE9QUElORxAEEhkKFVNBTkRCT1hfU1RBVEVfU1RPUFBFRBAFEhkKFVNBTkRCT1hfU1RBVEVfUEFVU0lORxAGEhgKFFNBTkRCT1hfU1RBVEVfUEFVU0VEEAcSGwoXU0FOREJPWF9TVEFURV9BUkNISVZJTkcQCBIaChZTQU5EQk9YX1NUQVRFX0FSQ0hJVkVEEAkSHQoZU0FOREJPWF9TVEFURV9URVJNSU5BVElORxAKEhwKGFNBTkRCT1hfU1RBVEVfVEVSTUlOQVRFRBALEhoKFlNBTkRCT1hfU1RBVEVfREVMRVRJTkcQDBIZChVTQU5EQk9YX1NUQVRFX0RFTEVURUQQDRIXChNTQU5EQk9YX1NUQVRFX0VSUk9SEA4qnwIKE0xpZmVjeWNsZUNhcGFiaWxpdHkSIAocTElGRUNZQ0xFX0NBUEFCSUxJVFlfVU5LTk9XThAAEiAKHExJRkVDWUNMRV9DQVBBQklMSVRZX1JFRlJFU0gQARIeChpMSUZFQ1lDTEVfQ0FQQUJJTElUWV9TVEFSVBACEh0KGUxJRkVDWUNMRV9DQVBBQklMSVRZX1NUT1AQAxIeChpMSUZFQ1lDTEVfQ0FQQUJJTElUWV9QQVVTRRAEEiIKHkxJRkVDWUNMRV9DQVBBQklMSVRZX1RFUk1JTkFURRAFEiAKHExJRkVDWUNMRV9DQVBBQklMSVRZX0FSQ0hJVkUQBhIfChtMSUZFQ1lDTEVfQ0FQQUJJTElUWV9ERUxFVEUQByreAwoJRXJyb3JLaW5kEhYKEkVSUk9SX0tJTkRfVU5LTk9XThAAEh8KG0VSUk9SX0tJTkRfSU5WQUxJRF9BUkdVTUVOVBABEhoKFkVSUk9SX0tJTkRfVU5TVVBQT1JURUQQAhIdChlFUlJPUl9LSU5EX0FVVEhFTlRJQ0FUSU9OEAMSIAocRVJST1JfS0lORF9QRVJNSVNTSU9OX0RFTklFRBAEEhgKFEVSUk9SX0tJTkRfTk9UX0ZPVU5EEAUSHQoZRVJST1JfS0lORF9BTFJFQURZX0VYSVNUUxAGEhcKE0VSUk9SX0tJTkRfQ09ORkxJQ1QQBxIbChdFUlJPUl9LSU5EX1JBVEVfTElNSVRFRBAIEiEKHUVSUk9SX0tJTkRfUkVTT1VSQ0VfRVhIQVVTVEVEEAkSFgoSRVJST1JfS0lORF9USU1FT1VUEAoSFwoTRVJST1JfS0lORF9DQU5DRUxFRBALEhoKFkVSUk9SX0tJTkRfVU5BVkFJTEFCTEUQDBIfChtFUlJPUl9LSU5EX0lOVkFMSURfUkVTUE9OU0UQDRIXChNFUlJPUl9LSU5EX0lOVEVSTkFMEA4SIgoeRVJST1JfS0lORF9GQUlMRURfUFJFQ09ORElUSU9OEA9CUFpOZ2l0aHViLmNvbS9zYW5kYm94LWtpdC9raXQvc2Rrcy9nby9pbnRlcm5hbC9jb250cmFjdHMva2l0L3NhbmRib3gvdjE7c2FuZGJveHYxYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_duration, file_google_protobuf_timestamp]); /** * CreateOptions is the creation request. It does not include lifecycle operations or credentials. @@ -1217,6 +1217,87 @@ export type RemoveOptions = Message<"kit.sandbox.v1.RemoveOptions"> & { export const RemoveOptionsSchema: GenMessage = /*@__PURE__*/ messageDesc(file_kit_sandbox_v1_sandbox, 36); +/** + * ExecOptions configures Process.Run. Set exactly one of argv or shell. + * Kit validates that exclusivity before the provider call. + * Nil timeout inherits Config.Timeout as the Kit deadline. Explicit zero adds + * no Kit deadline. Both omit and zero mean no provider command limit; a + * provider may send an explicit zero when its omitted native default would + * impose one. Provider command timeouts must be whole seconds. + * + * @generated from message kit.sandbox.v1.ExecOptions + */ +export type ExecOptions = Message<"kit.sandbox.v1.ExecOptions"> & { + /** + * Program and arguments. Mutually exclusive with shell. Must be non-empty when used. + * + * @generated from field: repeated string argv = 1; + */ + argv: string[]; + + /** + * Shell command string. Mutually exclusive with argv. + * + * @generated from field: optional string shell = 2; + */ + shell?: string | undefined; + + /** + * Absolute working directory inside the sandbox. + * + * @generated from field: optional string working_directory = 3; + */ + workingDirectory?: string | undefined; + + /** + * Environment variables for the command. An empty map is explicit; nil means omitted. + * + * @generated from field: map environment = 4; + */ + environment: { [key: string]: string }; + + /** + * @generated from field: google.protobuf.Duration timeout = 5; + */ + timeout?: Duration | undefined; +}; + +/** + * Describes the message kit.sandbox.v1.ExecOptions. + * Use `create(ExecOptionsSchema)` to create a new message. + */ +export const ExecOptionsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_kit_sandbox_v1_sandbox, 37); + +/** + * ExecResult is the outcome of Process.Run. A non-zero exit_code is not an error. + * + * @generated from message kit.sandbox.v1.ExecResult + */ +export type ExecResult = Message<"kit.sandbox.v1.ExecResult"> & { + /** + * @generated from field: int32 exit_code = 1; + */ + exitCode: number; + + /** + * @generated from field: bytes stdout = 2; + */ + stdout: Uint8Array; + + /** + * @generated from field: bytes stderr = 3; + */ + stderr: Uint8Array; +}; + +/** + * Describes the message kit.sandbox.v1.ExecResult. + * Use `create(ExecResultSchema)` to create a new message. + */ +export const ExecResultSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_kit_sandbox_v1_sandbox, 38); + /** * SandboxInfo is shared metadata. Optional fields stay absent when the provider did not report them. * @@ -1306,7 +1387,7 @@ export type SandboxInfo = Message<"kit.sandbox.v1.SandboxInfo"> & { * Use `create(SandboxInfoSchema)` to create a new message. */ export const SandboxInfoSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kit_sandbox_v1_sandbox, 37); + messageDesc(file_kit_sandbox_v1_sandbox, 39); /** * ErrorInfo is the portable error detail. The native cause stays on the runtime error and is not serialized. @@ -1392,7 +1473,7 @@ export type ErrorInfo = Message<"kit.sandbox.v1.ErrorInfo"> & { * Use `create(ErrorInfoSchema)` to create a new message. */ export const ErrorInfoSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kit_sandbox_v1_sandbox, 38); + messageDesc(file_kit_sandbox_v1_sandbox, 40); /** * Exact numeric alternatives avoid protobuf Struct's double-only representation. @@ -1460,7 +1541,7 @@ export type MetadataValue = Message<"kit.sandbox.v1.MetadataValue"> & { * Use `create(MetadataValueSchema)` to create a new message. */ export const MetadataValueSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kit_sandbox_v1_sandbox, 39); + messageDesc(file_kit_sandbox_v1_sandbox, 41); /** * MetadataList is an ordered list of metadata values. @@ -1481,7 +1562,7 @@ export type MetadataList = Message<"kit.sandbox.v1.MetadataList"> & { * Use `create(MetadataListSchema)` to create a new message. */ export const MetadataListSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kit_sandbox_v1_sandbox, 40); + messageDesc(file_kit_sandbox_v1_sandbox, 42); /** * MetadataObject is a string-keyed metadata object. Adapters fill values directly. @@ -1502,7 +1583,7 @@ export type MetadataObject = Message<"kit.sandbox.v1.MetadataObject"> & { * Use `create(MetadataObjectSchema)` to create a new message. */ export const MetadataObjectSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kit_sandbox_v1_sandbox, 41); + messageDesc(file_kit_sandbox_v1_sandbox, 43); /** * SnapshotKind classifies a snapshot. Preserve the numeric values. diff --git a/sdks/typescript/providers/daytona/README.md b/sdks/typescript/providers/daytona/README.md index 4b1ee427..05191a87 100644 --- a/sdks/typescript/providers/daytona/README.md +++ b/sdks/typescript/providers/daytona/README.md @@ -84,6 +84,14 @@ found, and the error field is `local_path`. A local directory is an invalid argument with the same field. `copyToLocal` creates a missing local parent directory. +**Commands** through `sandbox.process.run`. Set exactly one of `argv` or +`shell`. `argv` is sent as a POSIX-quoted shell string. A non-zero exit code +is not an SDK error—check `exitCode`. Daytona returns stdout only; `stderr` is +empty. Omit or zero `timeout` so the command is not capped by Daytona's +10-second native default. Command timeouts must be whole seconds (`2000` is +two seconds; `1500` is rejected). Aborting the signal rejects the `await`; the +remote command may keep running. + Daytona does not support unverified `attach`. Use `client.get(id)` for a provider-verified handle. @@ -93,13 +101,16 @@ inherit snapshot resources. ## Examples -| Example | Project | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| Creation | [create-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-sandbox) | -| Read a file | [read-daytona-file](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/read-daytona-file) | -| Policies | [create-daytona-with-policies](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-policies) | -| Retrieval | [get-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/lifecycle/get-daytona-sandbox) | -| Full index | [TypeScript examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript) | +| Example | Project | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| Creation | [create-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-sandbox) | +| Run a command | [run-daytona-command](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process/run-daytona-command) | +| Process | [process examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process) | +| Read a file | [read-daytona-file](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/read-daytona-file) | +| Filesystem | [filesystem examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem) | +| Policies | [create-daytona-with-policies](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-policies) | +| Retrieval | [get-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/lifecycle/get-daytona-sandbox) | +| Full index | [TypeScript examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript) | ## License diff --git a/sdks/typescript/providers/daytona/src/daytona.test.ts b/sdks/typescript/providers/daytona/src/daytona.test.ts index 8a6434f7..3c74b997 100644 --- a/sdks/typescript/providers/daytona/src/daytona.test.ts +++ b/sdks/typescript/providers/daytona/src/daytona.test.ts @@ -31,6 +31,7 @@ import { FileFieldLocalPath, FileFieldParents, CreateFieldProvisioningTimeout, + ExecFieldTimeout, } from "@sandbox-kit/sandbox"; import type { CreateOptions } from "@sandbox-kit/sandbox"; import { describe, expect, it } from "vitest"; @@ -744,4 +745,86 @@ describe("Daytona provider orchestration", () => { operation: "copy_to_local", }); }); + + it("maps Process.run argv, timeouts, and empty stderr for Daytona", async () => { + const seen: Array<{ + command: string; + cwd?: string; + env?: Record; + timeout?: number; + }> = []; + const backend = new DaytonaProvider(() => ({ + async create(): Promise { + throw new Error("native create"); + }, + async get(): Promise { + return { + process: { + async executeCommand( + command: string, + cwd?: string, + env?: Record, + timeout?: number, + ) { + seen.push({ + command, + ...(cwd === undefined ? {} : { cwd }), + ...(env === undefined ? {} : { env }), + ...(timeout === undefined ? {} : { timeout }), + }); + return { exitCode: 0, result: "ok\n" }; + }, + }, + } as unknown as DaytonaSandbox; + }, + async [Symbol.asyncDispose](): Promise {}, + })).initialize({}); + if (backend.run === undefined) throw new Error("run is missing"); + + const argvResult = await backend.run( + "sandbox-1", + { argv: ["echo", "hello world", "it's"] }, + {}, + ); + await backend.run("sandbox-1", { shell: "echo hi", timeout: 0 }, {}); + await backend.run( + "sandbox-1", + { + argv: ["pwd"], + workingDirectory: "/workspace", + environment: { KIT_DEMO: "from-kit" }, + timeout: 5_000, + }, + {}, + ); + await expect( + backend.run("sandbox-1", { argv: ["true"], timeout: 1_500 }, {}), + ).rejects.toMatchObject({ + kind: ErrorKind.InvalidArgument, + field: ExecFieldTimeout, + operation: "run", + }); + + expect(argvResult).toEqual({ + exitCode: 0, + stdout: new TextEncoder().encode("ok\n"), + stderr: new Uint8Array(), + }); + expect(seen).toEqual([ + { + command: `'echo' 'hello world' 'it'"'"'s'`, + timeout: 0, + }, + { + command: "echo hi", + timeout: 0, + }, + { + command: `'pwd'`, + cwd: "/workspace", + env: { KIT_DEMO: "from-kit" }, + timeout: 5, + }, + ]); + }); }); diff --git a/sdks/typescript/providers/daytona/src/daytona.ts b/sdks/typescript/providers/daytona/src/daytona.ts index 3ebd8ee6..278584d8 100644 --- a/sdks/typescript/providers/daytona/src/daytona.ts +++ b/sdks/typescript/providers/daytona/src/daytona.ts @@ -10,6 +10,7 @@ import { ConfigFieldTimeout, CreateFieldProvisioningTimeout, ErrorKind, + ExecFieldTimeout, FileFieldParents, FileType, type FileInfo, @@ -24,6 +25,8 @@ import type { CreateResult, ArchiveOptions, DeleteOptions, + ExecOptions, + ExecResult, FileCopyOptions, FileReadOptions, FileWriteOptions, @@ -350,6 +353,36 @@ class DaytonaBackend extends BackendBase implements Backend { }); } + async run( + id: string, + request: ExecOptions, + options: OperationOptions, + ): Promise { + try { + throwIfAborted(options.signal); + const timeout = wholeSeconds(request.timeout, "run", ExecFieldTimeout); + const sandbox = await this.native.get(id); + throwIfAborted(options.signal); + const command = daytonaCommand(request); + // Omitted and explicit zero become 0 so Daytona adds no command limit + // (its omitted default is 10 seconds). + const seconds = timeout ?? 0; + const response = await sandbox.process.executeCommand( + command, + request.workingDirectory, + request.environment, + seconds, + ); + return { + exitCode: response.exitCode, + stdout: new TextEncoder().encode(response.result ?? ""), + stderr: new Uint8Array(), + }; + } catch (error) { + throw mapDaytonaError(error, "run"); + } + } + async #file( operation: string, id: string, @@ -447,6 +480,20 @@ function wholeSeconds( return value / 1_000; } +function daytonaCommand(request: ExecOptions): string { + if (request.shell !== undefined) return request.shell; + return posixShellJoin(request.argv ?? []); +} + +function posixShellJoin(argv: readonly string[]): string { + return argv.map(posixQuote).join(" "); +} + +function posixQuote(value: string): string { + if (value === "") return "''"; + return "'" + value.replaceAll("'", `'"'"'`) + "'"; +} + /** Selects Daytona as the provider for the shared Sandbox Kit client. */ export function daytona(): Provider { return new DaytonaProvider(); diff --git a/sdks/typescript/providers/modal/README.md b/sdks/typescript/providers/modal/README.md index 8c3815b7..77154671 100644 --- a/sdks/typescript/providers/modal/README.md +++ b/sdks/typescript/providers/modal/README.md @@ -76,18 +76,29 @@ missing parents are created, and an existing directory is accepted. `local_path`. A local directory is an invalid argument with the same field. `copyToLocal` creates a missing local parent directory. +**Commands** through `sandbox.process.run`. Set exactly one of `argv` or +`shell`. `shell` runs as `bash -c`. A non-zero exit code is not an SDK +error—check `exitCode`. Modal returns both stdout and stderr. Omit or zero +`timeout` for no Modal command limit (the client omits `timeoutMs`; sending +`0` is rejected by Modal). Command timeouts must be whole seconds (`2000` is +two seconds; `1500` is rejected). Aborting the signal rejects the `await`; the +remote command may keep running. + When you set resources, Modal requires at least 0.125 CPU cores and 128 MiB memory. Omitted values keep Modal defaults. Disk size cannot be raised at create. ## Examples -| Example | Project | -| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| Creation | [create-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-sandbox) | -| Read a file | [read-modal-file](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/read-modal-file) | -| Resources | [create-modal-with-resources](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-with-resources) | -| Attach | [attach-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/lifecycle/attach-modal-sandbox) | -| Full index | [TypeScript examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript) | +| Example | Project | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Creation | [create-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-sandbox) | +| Run a command | [run-modal-command](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process/run-modal-command) | +| Process | [process examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process) | +| Read a file | [read-modal-file](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/read-modal-file) | +| Filesystem | [filesystem examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem) | +| Resources | [create-modal-with-resources](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-with-resources) | +| Attach | [attach-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/lifecycle/attach-modal-sandbox) | +| Full index | [TypeScript examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript) | ## License diff --git a/sdks/typescript/providers/modal/src/modal.test.ts b/sdks/typescript/providers/modal/src/modal.test.ts index 43e5617b..0eb5085f 100644 --- a/sdks/typescript/providers/modal/src/modal.test.ts +++ b/sdks/typescript/providers/modal/src/modal.test.ts @@ -548,4 +548,65 @@ describe("Modal provider orchestration", () => { }); expect(lookedUp).toBe(false); }); + + it("omits Modal exec timeoutMs for an omitted or zero timeout", async () => { + const seen: Array = []; + const nativeSandbox = { + async exec(_command: string[], params?: { readonly timeoutMs?: number }) { + seen.push(params?.timeoutMs); + throw new Error("stop after timeout is recorded"); + }, + } as unknown as ModalSandbox; + const backend = new ModalProvider(() => ({ + apps: { + async fromName(): Promise { + throw new Error("unexpected app lookup"); + }, + }, + images: { + fromRegistry(): Image { + return {} as Image; + }, + }, + proxies: { + async fromName(): Promise { + throw new Error("unexpected proxy lookup"); + }, + }, + secrets: { + async fromName(): Promise { + throw new Error("unexpected secret lookup"); + }, + }, + sandboxes: { + async create(): Promise { + throw new Error("unexpected create"); + }, + async fromId(): Promise { + return nativeSandbox; + }, + }, + close(): void {}, + })).initialize({ scope: { appName: "app" } }); + if (backend.run === undefined) throw new Error("run is missing"); + + await expect( + backend.run("sandbox-1", { argv: ["echo", "hi"] }, {}), + ).rejects.toThrow("stop after timeout is recorded"); + await expect( + backend.run("sandbox-1", { argv: ["echo", "hi"], timeout: 0 }, {}), + ).rejects.toThrow("stop after timeout is recorded"); + await expect( + backend.run("sandbox-1", { argv: ["echo", "hi"], timeout: 5_000 }, {}), + ).rejects.toThrow("stop after timeout is recorded"); + await expect( + backend.run("sandbox-1", { argv: ["echo", "hi"], timeout: 1_500 }, {}), + ).rejects.toMatchObject({ + kind: ErrorKind.InvalidArgument, + field: "timeout", + operation: "run", + }); + + expect(seen).toEqual([undefined, undefined, 5_000]); + }); }); diff --git a/sdks/typescript/providers/modal/src/modal.ts b/sdks/typescript/providers/modal/src/modal.ts index 398a84c1..ba5a3e7c 100644 --- a/sdks/typescript/providers/modal/src/modal.ts +++ b/sdks/typescript/providers/modal/src/modal.ts @@ -5,6 +5,8 @@ import { type Sandbox as ModalSandbox, } from "modal"; import { + ErrorKind, + ExecFieldTimeout, FileType, type FileInfo, type OperationOptions, @@ -16,6 +18,8 @@ import type { Config, CreateOptions, CreateResult, + ExecOptions, + ExecResult, ExitResult, FileCopyOptions, FileReadOptions, @@ -33,6 +37,7 @@ import { } from "#mappings/mappings.js"; import { mapModalError, + providerError, requireLocalDestination, requireLocalFile, translateLocalCopyError, @@ -163,7 +168,7 @@ class ModalBackend extends BackendBase implements Backend { _request: FileReadOptions, options: OperationOptions, ): Promise { - return this.#file("read_file", id, options, async (sandbox) => + return this.#withSandbox("read_file", id, options, async (sandbox) => sandbox.filesystem.readBytes(path), ); } @@ -175,7 +180,7 @@ class ModalBackend extends BackendBase implements Backend { _request: FileWriteOptions, options: OperationOptions, ): Promise { - await this.#file("write_file", id, options, async (sandbox) => { + await this.#withSandbox("write_file", id, options, async (sandbox) => { await sandbox.filesystem.writeBytes(data, path); }); } @@ -188,7 +193,7 @@ class ModalBackend extends BackendBase implements Backend { options: OperationOptions, ): Promise { await requireLocalFile(localPath, "copy_from_local"); - await this.#file("copy_from_local", id, options, async (sandbox) => { + await this.#withSandbox("copy_from_local", id, options, async (sandbox) => { try { await sandbox.filesystem.copyFromLocal(localPath, remotePath); } catch (error) { @@ -205,7 +210,7 @@ class ModalBackend extends BackendBase implements Backend { options: OperationOptions, ): Promise { await requireLocalDestination(localPath, "copy_to_local"); - await this.#file("copy_to_local", id, options, async (sandbox) => { + await this.#withSandbox("copy_to_local", id, options, async (sandbox) => { try { await sandbox.filesystem.copyToLocal(remotePath, localPath); } catch (error) { @@ -220,7 +225,7 @@ class ModalBackend extends BackendBase implements Backend { request: MakeDirectoryOptions, options: OperationOptions, ): Promise { - await this.#file("make_directory", id, options, async (sandbox) => { + await this.#withSandbox("make_directory", id, options, async (sandbox) => { await sandbox.filesystem.makeDirectory(path, { createParents: request.parents ?? true, }); @@ -233,7 +238,7 @@ class ModalBackend extends BackendBase implements Backend { _request: ListDirectoryOptions, options: OperationOptions, ): Promise { - return this.#file("list_directory", id, options, async (sandbox) => { + return this.#withSandbox("list_directory", id, options, async (sandbox) => { const entries = await sandbox.filesystem.listFiles(path); return entries.map((entry) => modalFileInfo(entry)); }); @@ -245,7 +250,7 @@ class ModalBackend extends BackendBase implements Backend { _request: StatOptions, options: OperationOptions, ): Promise { - return this.#file("stat", id, options, async (sandbox) => + return this.#withSandbox("stat", id, options, async (sandbox) => modalFileInfo(await sandbox.filesystem.stat(path)), ); } @@ -256,14 +261,45 @@ class ModalBackend extends BackendBase implements Backend { request: RemoveOptions, options: OperationOptions, ): Promise { - await this.#file("remove", id, options, async (sandbox) => { + await this.#withSandbox("remove", id, options, async (sandbox) => { await sandbox.filesystem.remove(path, { recursive: request.recursive === true, }); }); } - async #file( + async run( + id: string, + request: ExecOptions, + options: OperationOptions, + ): Promise { + return this.#withSandbox("run", id, options, async (sandbox) => { + const command = modalCommand(request); + const timeoutMs = execTimeoutMs(request.timeout); + const process = await sandbox.exec(command, { + mode: "binary", + stdout: "pipe", + stderr: "pipe", + // Modal rejects an explicit timeoutMs of 0; omit the field for no limit. + ...(timeoutMs === undefined ? {} : { timeoutMs }), + ...(request.workingDirectory === undefined + ? {} + : { workdir: request.workingDirectory }), + ...(request.environment === undefined + ? {} + : { env: request.environment }), + }); + await process.closeStdin(); + const [stdout, stderr, exitCode] = await Promise.all([ + process.stdout.readBytes(), + process.stderr.readBytes(), + process.wait(), + ]); + return { exitCode, stdout, stderr }; + }); + } + + async #withSandbox( operation: string, id: string, options: OperationOptions, @@ -359,6 +395,29 @@ function abortableDelay( }); } +function modalCommand(request: ExecOptions): string[] { + if (request.shell !== undefined) return ["bash", "-c", request.shell]; + return [...(request.argv ?? [])]; +} + +/** + * Maps Kit ExecOptions.timeout (ms) to Modal's timeoutMs. + * Omitted and explicit zero become undefined so Kit omits timeoutMs (Modal + * rejects an explicit 0). Non-zero values must be whole seconds. + */ +function execTimeoutMs(timeout: number | undefined): number | undefined { + if (timeout === undefined || timeout === 0) return undefined; + if (!Number.isFinite(timeout) || timeout < 0 || timeout % 1_000 !== 0) { + providerError({ + kind: ErrorKind.InvalidArgument, + operation: "run", + field: ExecFieldTimeout, + message: "timeout must be a whole number of seconds", + }); + } + return timeout; +} + /** Selects Modal as the provider for the shared Sandbox Kit client. */ export function modal(): Provider { return new ModalProvider(); diff --git a/sdks/typescript/sandbox/README.md b/sdks/typescript/sandbox/README.md index a77a40a5..2eac31c1 100644 --- a/sdks/typescript/sandbox/README.md +++ b/sdks/typescript/sandbox/README.md @@ -74,6 +74,37 @@ const attached = await client.attach(id); // provider must support attach const fetched = await client.get(id); // provider-verified lookup ``` +## Run a command + +```ts +const result = await sandbox.process.run({ + argv: ["echo", "hello from kit"], +}); +console.log(result.exitCode, new TextDecoder().decode(result.stdout)); +``` + +Set exactly one of `argv` or `shell`. A non-zero exit code is not an SDK +error—check `exitCode`. Optional `workingDirectory` must be absolute. +`environment` applies to that call only. Omit `timeout` to use the client +deadline; use `0` for no extra deadline on this call. Aborting the signal +rejects the `await`; the remote command may keep running. See the +[process guide](https://github.com/sandbox-kit/kit/blob/main/docs/process.md). + +## Read and write files + +```ts +await sandbox.filesystem.writeFile( + "/tmp/note.txt", + new TextEncoder().encode("hello\n"), +); +const data = await sandbox.filesystem.readFile("/tmp/note.txt"); +console.log(new TextDecoder().decode(data)); +``` + +Sandbox paths are absolute. The same handle also supports `copyFromLocal`, +`copyToLocal`, `makeDirectory`, `listDirectory`, `stat`, and `remove`. See the +[filesystem guide](https://github.com/sandbox-kit/kit/blob/main/docs/filesystem.md). + ## Sandbox handle | Method | Purpose | @@ -91,6 +122,7 @@ const fetched = await client.get(id); // provider-verified lookup | `terminate` | Terminate (`wait` when supported) | | `close` | Close the handle. The sandbox keeps running | | `filesystem` | Read, write, copy, list, mkdir, stat, remove | +| `process` | Run a command and wait for exit code + output | `supports` reports whether the provider implements that operation. An unimplemented method throws before the provider call. A provider rejection, diff --git a/sdks/typescript/sandbox/src/client/client.test.ts b/sdks/typescript/sandbox/src/client/client.test.ts index eaece14b..d44febc9 100644 --- a/sdks/typescript/sandbox/src/client/client.test.ts +++ b/sdks/typescript/sandbox/src/client/client.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; import { + ExecFieldArgv, + ExecFieldShell, + ExecFieldWorkingDirectory, FileFieldLocalPath, FileFieldPath, CreateFieldLifetimeIdleStopAfter, @@ -45,6 +48,9 @@ class RecordingBackend implements Backend { createCalls = 0; terminated = false; lastCreateRequest: CreateOptions | undefined; + // Optional Backend methods tests attach at runtime. + run?: Backend["run"]; + readFile?: Backend["readFile"]; async create( request: CreateOptions, @@ -628,6 +634,130 @@ describe("Sandbox lifecycle", () => { expect(maximumActive).toBe(1); }); + it("holds serialization until an aborted in-flight provider call settles", async () => { + let active = 0; + let maximumActive = 0; + let releaseFirst!: () => void; + const firstBlocked = new Promise((resolve) => { + releaseFirst = resolve; + }); + const backend: Backend = { + name: "serial-inflight-abort", + close: async () => {}, + closeSandbox: async () => {}, + attach: async (id) => ({ + id, + provider: "serial-inflight-abort", + state: SandboxState.Running, + }), + start: async (id) => { + active++; + maximumActive = Math.max(maximumActive, active); + await firstBlocked; + active--; + return { + id, + provider: "serial-inflight-abort", + state: SandboxState.Running, + }; + }, + }; + const client = new Client({ + provider: { name: backend.name, initialize: () => backend }, + }); + const sandbox = await client.attach("sandbox-existing"); + const controller = new AbortController(); + + const first = sandbox.start({ signal: controller.signal }); + await Promise.resolve(); + controller.abort(); + await expect(first).rejects.toMatchObject({ kind: ErrorKind.Canceled }); + + const second = sandbox.start(); + await Promise.resolve(); + expect(active).toBe(1); + expect(maximumActive).toBe(1); + + releaseFirst(); + await second; + expect(maximumActive).toBe(1); + }); + + it("holds process serialization until an aborted in-flight run settles", async () => { + let active = 0; + let maximumActive = 0; + let releaseFirst!: () => void; + const firstBlocked = new Promise((resolve) => { + releaseFirst = resolve; + }); + const { client, backend } = fixture(); + backend.run = async () => { + active++; + maximumActive = Math.max(maximumActive, active); + await firstBlocked; + active--; + return { + exitCode: 0, + stdout: new Uint8Array(), + stderr: new Uint8Array(), + }; + }; + const sandbox = await client.create(); + const controller = new AbortController(); + + const first = sandbox.process.run({ + argv: ["sleep", "1"], + signal: controller.signal, + }); + await Promise.resolve(); + controller.abort(); + await expect(first).rejects.toMatchObject({ kind: ErrorKind.Canceled }); + + const second = sandbox.process.run({ argv: ["true"] }); + await Promise.resolve(); + expect(active).toBe(1); + expect(maximumActive).toBe(1); + + releaseFirst(); + await second; + expect(maximumActive).toBe(1); + }); + + it("holds filesystem serialization until an aborted in-flight read settles", async () => { + let active = 0; + let maximumActive = 0; + let releaseFirst!: () => void; + const firstBlocked = new Promise((resolve) => { + releaseFirst = resolve; + }); + const { client, backend } = fixture(); + backend.readFile = async () => { + active++; + maximumActive = Math.max(maximumActive, active); + await firstBlocked; + active--; + return new TextEncoder().encode("ok"); + }; + const sandbox = await client.create(); + const controller = new AbortController(); + + const first = sandbox.filesystem.readFile("/tmp/a.txt", { + signal: controller.signal, + }); + await Promise.resolve(); + controller.abort(); + await expect(first).rejects.toMatchObject({ kind: ErrorKind.Canceled }); + + const second = sandbox.filesystem.readFile("/tmp/b.txt"); + await Promise.resolve(); + expect(active).toBe(1); + expect(maximumActive).toBe(1); + + releaseFirst(); + await second; + expect(maximumActive).toBe(1); + }); + it("rejects a relative sandbox path before the provider call", async () => { const { client, backend } = fixture(); const files: Backend = backend; @@ -637,7 +767,9 @@ describe("Sandbox lifecycle", () => { return new Uint8Array(); }; const sandbox = await client.create(); - await expect(sandbox.filesystem.readFile("relative.txt")).rejects.toMatchObject({ + await expect( + sandbox.filesystem.readFile("relative.txt"), + ).rejects.toMatchObject({ kind: ErrorKind.InvalidArgument, operation: "read_file", field: FileFieldPath, @@ -677,4 +809,81 @@ describe("Sandbox lifecycle", () => { operation: "stat", }); }); + + it("rejects a process call without argv or shell before the provider", async () => { + const { client, backend } = fixture(); + let calls = 0; + backend.run = async () => { + calls++; + return { + exitCode: 0, + stdout: new Uint8Array(), + stderr: new Uint8Array(), + }; + }; + const sandbox = await client.create(); + await expect(sandbox.process.run({})).rejects.toMatchObject({ + kind: ErrorKind.InvalidArgument, + operation: "run", + field: ExecFieldArgv, + }); + expect(calls).toBe(0); + }); + + it("rejects a blank shell before the provider", async () => { + const { client, backend } = fixture(); + backend.run = async () => { + throw new Error("provider was called"); + }; + const sandbox = await client.create(); + for (const shell of ["", " "]) { + await expect(sandbox.process.run({ shell })).rejects.toMatchObject({ + kind: ErrorKind.InvalidArgument, + operation: "run", + field: ExecFieldShell, + }); + } + }); + + it("rejects a relative working directory before the provider", async () => { + const { client, backend } = fixture(); + backend.run = async () => { + throw new Error("provider was called"); + }; + const sandbox = await client.create(); + await expect( + sandbox.process.run({ argv: ["pwd"], workingDirectory: "relative" }), + ).rejects.toMatchObject({ + kind: ErrorKind.InvalidArgument, + operation: "run", + field: ExecFieldWorkingDirectory, + }); + }); + + it("runs argv through the provider and keeps a non-zero exit code", async () => { + const { client, backend } = fixture(); + backend.run = async (_id, request) => { + expect(request.argv).toEqual(["echo", "hello"]); + return { + exitCode: 7, + stdout: new TextEncoder().encode("hello\n"), + stderr: new Uint8Array(), + }; + }; + const sandbox = await client.create(); + const result = await sandbox.process.run({ argv: ["echo", "hello"] }); + expect(result.exitCode).toBe(7); + expect(new TextDecoder().decode(result.stdout)).toBe("hello\n"); + }); + + it("reports process methods the provider does not implement", async () => { + const { client } = fixture(); + const sandbox = await client.create(); + await expect(sandbox.process.run({ argv: ["true"] })).rejects.toMatchObject( + { + kind: ErrorKind.Unsupported, + operation: "run", + }, + ); + }); }); diff --git a/sdks/typescript/sandbox/src/client/client.ts b/sdks/typescript/sandbox/src/client/client.ts index df8ca80d..cf9560c5 100644 --- a/sdks/typescript/sandbox/src/client/client.ts +++ b/sdks/typescript/sandbox/src/client/client.ts @@ -1,7 +1,7 @@ // Client runtime behavior is handwritten. Portable request and response data // remain generated from protobuf. This module owns construction and create / // attach / get / close; validation, errors, and the Sandbox handle -// (lifecycle + filesystem) live beside it. +// (lifecycle + filesystem + process) live beside it. import type { CreateOptions, @@ -42,6 +42,7 @@ export type { } from "#client/types.js"; export { ErrorKind, SandboxError, sandboxError } from "#client/errors.js"; export { Filesystem } from "#client/filesystem.js"; +export { Process } from "#client/process.js"; export { Sandbox } from "#client/sandbox.js"; export { validateConfig, validateCreateOptions } from "#client/validation.js"; diff --git a/sdks/typescript/sandbox/src/client/filesystem.ts b/sdks/typescript/sandbox/src/client/filesystem.ts index 97dad718..7f1bed70 100644 --- a/sdks/typescript/sandbox/src/client/filesystem.ts +++ b/sdks/typescript/sandbox/src/client/filesystem.ts @@ -13,7 +13,7 @@ import type { StatOptions, } from "#contracts/sandbox_data_contracts.js"; import { ErrorKind, sandboxError, unsupported } from "#client/errors.js"; -import { awaitWithSignal, operationOptions } from "#client/operation.js"; +import { operationOptions } from "#client/operation.js"; import type { Backend, OperationOptions } from "#client/types.js"; type FileCall = T & OperationOptions; @@ -46,16 +46,12 @@ export class Filesystem { return this.#call("read_file", path, options, (signal) => { const method = this.host.backend.readFile; if (!method) unsupported(this.host.providerName, "read_file"); - return awaitWithSignal( - () => - method.call( - this.host.backend, - this.host.id, - path, - requestFields(options), - operationOptions(options, signal), - ), - signal, + return method.call( + this.host.backend, + this.host.id, + path, + requestFields(options), + operationOptions(options, signal), ); }); } @@ -69,17 +65,13 @@ export class Filesystem { return this.#call("write_file", path, options, (signal) => { const method = this.host.backend.writeFile; if (!method) unsupported(this.host.providerName, "write_file"); - return awaitWithSignal( - () => - method.call( - this.host.backend, - this.host.id, - path, - data, - requestFields(options), - operationOptions(options, signal), - ), - signal, + return method.call( + this.host.backend, + this.host.id, + path, + data, + requestFields(options), + operationOptions(options, signal), ); }); } @@ -98,17 +90,13 @@ export class Filesystem { (signal) => { const method = this.host.backend.copyFromLocal; if (!method) unsupported(this.host.providerName, "copy_from_local"); - return awaitWithSignal( - () => - method.call( - this.host.backend, - this.host.id, - localPath, - remotePath, - requestFields(options), - operationOptions(options, signal), - ), - signal, + return method.call( + this.host.backend, + this.host.id, + localPath, + remotePath, + requestFields(options), + operationOptions(options, signal), ); }, ); @@ -132,17 +120,13 @@ export class Filesystem { (signal) => { const method = this.host.backend.copyToLocal; if (!method) unsupported(this.host.providerName, "copy_to_local"); - return awaitWithSignal( - () => - method.call( - this.host.backend, - this.host.id, - remotePath, - localPath, - requestFields(options), - operationOptions(options, signal), - ), - signal, + return method.call( + this.host.backend, + this.host.id, + remotePath, + localPath, + requestFields(options), + operationOptions(options, signal), ); }, ); @@ -160,16 +144,12 @@ export class Filesystem { return this.#call("make_directory", path, options, (signal) => { const method = this.host.backend.makeDirectory; if (!method) unsupported(this.host.providerName, "make_directory"); - return awaitWithSignal( - () => - method.call( - this.host.backend, - this.host.id, - path, - directoryRequest(options), - operationOptions(options, signal), - ), - signal, + return method.call( + this.host.backend, + this.host.id, + path, + directoryRequest(options), + operationOptions(options, signal), ); }); } @@ -182,16 +162,12 @@ export class Filesystem { return this.#call("list_directory", path, options, (signal) => { const method = this.host.backend.listDirectory; if (!method) unsupported(this.host.providerName, "list_directory"); - return awaitWithSignal( - () => - method.call( - this.host.backend, - this.host.id, - path, - requestFields(options), - operationOptions(options, signal), - ), - signal, + return method.call( + this.host.backend, + this.host.id, + path, + requestFields(options), + operationOptions(options, signal), ); }); } @@ -201,16 +177,12 @@ export class Filesystem { return this.#call("stat", path, options, (signal) => { const method = this.host.backend.stat; if (!method) unsupported(this.host.providerName, "stat"); - return awaitWithSignal( - () => - method.call( - this.host.backend, - this.host.id, - path, - requestFields(options), - operationOptions(options, signal), - ), - signal, + return method.call( + this.host.backend, + this.host.id, + path, + requestFields(options), + operationOptions(options, signal), ); }); } @@ -223,16 +195,12 @@ export class Filesystem { return this.#call("remove", path, options, (signal) => { const method = this.host.backend.remove; if (!method) unsupported(this.host.providerName, "remove"); - return awaitWithSignal( - () => - method.call( - this.host.backend, - this.host.id, - path, - removeRequest(options), - operationOptions(options, signal), - ), - signal, + return method.call( + this.host.backend, + this.host.id, + path, + removeRequest(options), + operationOptions(options, signal), ); }); } diff --git a/sdks/typescript/sandbox/src/client/operation.ts b/sdks/typescript/sandbox/src/client/operation.ts index 0fee6266..1185b01b 100644 --- a/sdks/typescript/sandbox/src/client/operation.ts +++ b/sdks/typescript/sandbox/src/client/operation.ts @@ -65,12 +65,25 @@ function abortReason(signal: AbortSignal): DOMException { : new DOMException("The operation was aborted", "AbortError"); } +/** + * Resolves with `operation`, or rejects promptly when `signal` aborts. + * Pass a factory to start work only when the signal is not already aborted. + * Pass an already-started promise when the caller must keep that work alive + * (for example handle serialization after an in-flight abort). + */ export async function awaitWithSignal( - operation: () => Promise, + operation: (() => Promise) | Promise, signal?: AbortSignal, ): Promise { + if (typeof operation === "function") { + if (signal?.aborted) throw abortReason(signal); + return raceSignal(operation(), signal); + } + return raceSignal(operation, signal); +} + +function raceSignal(pending: Promise, signal?: AbortSignal): Promise { if (signal?.aborted) throw abortReason(signal); - const pending = operation(); if (signal === undefined) return pending; return new Promise((resolve, reject) => { const aborted = () => reject(abortReason(signal)); diff --git a/sdks/typescript/sandbox/src/client/process.ts b/sdks/typescript/sandbox/src/client/process.ts new file mode 100644 index 00000000..0388b245 --- /dev/null +++ b/sdks/typescript/sandbox/src/client/process.ts @@ -0,0 +1,118 @@ +// Process calls are handwritten. They validate argv/shell and working +// directory, then dispatch through optional Backend.run. + +import { + ExecFieldArgv, + ExecFieldShell, + ExecFieldWorkingDirectory, +} from "#contracts/field_paths.js"; +import type { + ExecOptions, + ExecResult, +} from "#contracts/sandbox_data_contracts.js"; +import { ErrorKind, sandboxError, unsupported } from "#client/errors.js"; +import { operationOptions } from "#client/operation.js"; +import type { Backend, OperationOptions } from "#client/types.js"; + +type ProcessCall = ExecOptions & OperationOptions; + +interface ProcessHost { + readonly providerName: string; + readonly id: string; + readonly backend: Backend; + run( + operation: string, + options: OperationOptions, + body: (signal: AbortSignal | undefined) => Promise, + ): Promise; +} + +/** + * Runs commands inside one sandbox and waits for each to finish. Set exactly + * one of `argv` or `shell`. A working directory, when set, must be absolute. + * An omitted operation timeout uses the client timeout; explicit zero adds no + * Kit deadline. + */ +export class Process { + constructor(private readonly host: ProcessHost) {} + + /** + * Runs one command and returns its exit code and collected output. + * A non-zero exit code is not an error. + */ + async run(options: ProcessCall): Promise { + validateExecOptions(this.host.providerName, "run", options); + return this.host.run("run", options, (signal) => { + const method = this.host.backend.run; + if (!method) unsupported(this.host.providerName, "run"); + return method.call( + this.host.backend, + this.host.id, + requestFields(options), + operationOptions(options, signal), + ); + }); + } +} + +function validateExecOptions( + provider: string, + operation: string, + options: ExecOptions, +): void { + const hasArgv = Array.isArray(options.argv) && options.argv.length > 0; + const hasShell = options.shell !== undefined; + if (hasArgv && hasShell) { + throw sandboxError({ + kind: ErrorKind.InvalidArgument, + provider, + operation, + field: ExecFieldArgv, + message: "sandbox-kit: set exactly one of argv or shell", + }); + } + if (!hasArgv && !hasShell) { + throw sandboxError({ + kind: ErrorKind.InvalidArgument, + provider, + operation, + field: ExecFieldArgv, + message: "sandbox-kit: set argv or shell", + }); + } + if (hasShell && options.shell!.trim() === "") { + throw sandboxError({ + kind: ErrorKind.InvalidArgument, + provider, + operation, + field: ExecFieldShell, + message: "sandbox-kit: shell must not be blank", + }); + } + if (options.workingDirectory !== undefined) { + const path = options.workingDirectory; + if (!path || !path.startsWith("/") || path.includes("\0")) { + throw sandboxError({ + kind: ErrorKind.InvalidArgument, + provider, + operation, + field: ExecFieldWorkingDirectory, + message: "sandbox-kit: working directory must be absolute", + }); + } + } +} + +function requestFields(options: ProcessCall): ExecOptions { + const request: ExecOptions = {}; + if (options.argv !== undefined) request.argv = options.argv; + if (options.shell !== undefined) request.shell = options.shell; + if (options.workingDirectory !== undefined) { + request.workingDirectory = options.workingDirectory; + } + if (options.environment !== undefined) { + request.environment = options.environment; + } + if (options.timeout !== undefined) request.timeout = options.timeout; + return request; +} diff --git a/sdks/typescript/sandbox/src/client/sandbox.ts b/sdks/typescript/sandbox/src/client/sandbox.ts index 3d9fc4ee..76ce87cc 100644 --- a/sdks/typescript/sandbox/src/client/sandbox.ts +++ b/sdks/typescript/sandbox/src/client/sandbox.ts @@ -1,5 +1,5 @@ -// Sandbox handle runtime behavior is handwritten. Lifecycle and filesystem -// methods dispatch through optional Backend methods. +// Sandbox handle runtime behavior is handwritten. Lifecycle, filesystem, and +// process methods dispatch through optional Backend methods. import { type ArchiveOptions, @@ -21,6 +21,7 @@ import { publicTimeoutOptions, } from "#client/operation.js"; import { Filesystem } from "#client/filesystem.js"; +import { Process } from "#client/process.js"; import type { Backend, DeleteCallOptions, @@ -43,8 +44,8 @@ export interface SandboxInit { /** * Handle for one cloud sandbox. * Lifecycle methods change it. `filesystem` exposes the shared file - * operations. `close` leaves the cloud sandbox as it is, and you can keep - * using the handle. + * operations. `process` runs commands. `close` leaves the cloud sandbox as + * it is, and you can keep using the handle. */ export class Sandbox implements AsyncDisposable { readonly #backend: Backend; @@ -54,6 +55,11 @@ export class Sandbox implements AsyncDisposable { * stat, and remove. Paths inside the sandbox are absolute. */ readonly filesystem: Filesystem; + /** + * Command runner for this sandbox. Set exactly one of `argv` or `shell`. + * A working directory, when set, must be absolute. + */ + readonly process: Process; #info: SandboxInfo; #tail: Promise = Promise.resolve(); constructor(token: typeof sandboxConstructorToken, init: SandboxInit) { @@ -62,18 +68,31 @@ export class Sandbox implements AsyncDisposable { this.#backend = init.backend; this.#info = clone(init.info); this.#timeout = init.timeout; - this.filesystem = new Filesystem({ - providerName: this.providerName, - id: this.id, - backend: this.#backend, - run: (operation, options, body) => - this.#serialized( + const self = this; + const host = { + get providerName() { + return self.providerName; + }, + get id() { + return self.id; + }, + get backend() { + return self.#backend; + }, + run: ( + operation: string, + options: OperationOptions, + body: (signal: AbortSignal | undefined) => Promise, + ): Promise => + self.#serialized( operation, options, - options.timeout ?? this.#timeout, + options.timeout ?? self.#timeout, body, ), - }); + }; + this.filesystem = new Filesystem(host); + this.process = new Process(host); } /** Sandbox identifier. */ get id(): string { @@ -89,15 +108,16 @@ export class Sandbox implements AsyncDisposable { } /** * Lifecycle operations this provider implements. - * File operations are not lifecycle capabilities; use {@link filesystem}. + * File and process operations are not lifecycle capabilities; use + * {@link filesystem} or {@link process}. */ capabilities(): readonly LifecycleCapability[] { return providerCapabilities(this.#backend); } /** * Reports whether this provider implements a lifecycle `capability`. - * File operations are checked by calling {@link filesystem} methods, not - * `supports`. + * File and process operations are checked by calling {@link filesystem} or + * {@link process} methods, not `supports`. */ supports(capability: LifecycleCapability): boolean { return this.capabilities().includes(capability); @@ -114,16 +134,13 @@ export class Sandbox implements AsyncDisposable { async (signal) => { const method = this.#backend.refresh; if (!method) unsupported(this.providerName, "refresh"); - const info = await awaitWithSignal( - () => - method.call( - this.#backend, - this.id, - operationOptions(options, signal), - ), - signal, + return this.#storeInfo( + await method.call( + this.#backend, + this.id, + operationOptions(options, signal), + ), ); - return this.#storeInfo(info); }, ); } @@ -188,17 +205,12 @@ export class Sandbox implements AsyncDisposable { async (signal) => { const method = this.#backend.terminate; if (!method) unsupported(this.providerName, "terminate"); - const result = await awaitWithSignal( - () => - method.call( - this.#backend, - this.id, - { ...request }, - operationOptions(call, signal), - ), - signal, + return method.call( + this.#backend, + this.id, + { ...request }, + operationOptions(call, signal), ); - return result; }, ); } @@ -238,16 +250,9 @@ export class Sandbox implements AsyncDisposable { * Closing a Daytona handle does not contact Daytona. Closing a Modal handle does not terminate the sandbox. */ async close(options: OperationOptions = {}): Promise { - await this.#serialized("close", options, this.#timeout, async (signal) => { - await awaitWithSignal( - () => - this.#backend.closeSandbox( - this.id, - operationOptions(options, signal), - ), - signal, - ); - }); + await this.#serialized("close", options, this.#timeout, (signal) => + this.#backend.closeSandbox(this.id, operationOptions(options, signal)), + ); } async [Symbol.asyncDispose](): Promise { await this.close(); @@ -280,17 +285,14 @@ export class Sandbox implements AsyncDisposable { options.timeout ?? this.#timeout, async (signal) => { if (!method) unsupported(this.providerName, operation); - const info = await awaitWithSignal( - () => - method.call( - this.#backend, - this.id, - { ...request }, - operationOptions(options, signal), - ), - signal, + return this.#storeInfo( + await method.call( + this.#backend, + this.id, + { ...request }, + operationOptions(options, signal), + ), ); - return this.#storeInfo(info); }, ); } @@ -300,6 +302,9 @@ export class Sandbox implements AsyncDisposable { this.#info = stored; return clone(stored); } + // One active provider call per handle. Abort/timeout still rejects the + // caller promptly (providers often ignore AbortSignal). The gate stays held + // until provider work settles so a follow-up call cannot overlap it. async #serialized( operation: string, options: OperationOptions, @@ -313,17 +318,26 @@ export class Sandbox implements AsyncDisposable { }); let acquired = false; let scoped: ReturnType | undefined; + let work: Promise | undefined; try { scoped = operationSignal(options, timeout); - await awaitWithSignal(() => previous, scoped.signal); + await awaitWithSignal(previous, scoped.signal); acquired = true; - return await body(scoped.signal); + work = body(scoped.signal); + // Race the already-started provider promise so abort rejects promptly + // without dropping that promise (unlock still waits on it below). + return await awaitWithSignal(work, scoped.signal); } catch (error) { throw withContext(error, this.providerName, operation); } finally { scoped?.dispose(); - if (acquired) unlock(); - else void previous.then(unlock, unlock); + if (!acquired) { + void previous.then(unlock, unlock); + } else { + // Do not await work here: that would delay abort/timeout rejection when + // the provider ignores the signal. Unlock when the native call settles. + void Promise.resolve(work).then(unlock, unlock); + } } } } diff --git a/sdks/typescript/sandbox/src/client/types.ts b/sdks/typescript/sandbox/src/client/types.ts index fae199d2..6298a7b0 100644 --- a/sdks/typescript/sandbox/src/client/types.ts +++ b/sdks/typescript/sandbox/src/client/types.ts @@ -16,6 +16,8 @@ import type { CreateResult, DeleteOptions, ExitResult, + ExecOptions, + ExecResult, FileCopyOptions, FileInfo, FileReadOptions, @@ -160,6 +162,11 @@ export interface Backend { request: RemoveOptions, options: OperationOptions, ): Promise; + run?( + id: string, + request: ExecOptions, + options: OperationOptions, + ): Promise; close(options: OperationOptions): Promise; closeSandbox(id: string, options: OperationOptions): Promise; } diff --git a/sdks/typescript/sandbox/src/contracts/field_paths.ts b/sdks/typescript/sandbox/src/contracts/field_paths.ts index e63dabeb..18fbb86a 100644 --- a/sdks/typescript/sandbox/src/contracts/field_paths.ts +++ b/sdks/typescript/sandbox/src/contracts/field_paths.ts @@ -380,3 +380,15 @@ export const FileFieldLocalPath = "local_path" as const; /** Canonical path "parents" on MakeDirectoryOptions. */ export const FileFieldParents = "parents" as const; + +/** Canonical path "argv" on ExecOptions. */ +export const ExecFieldArgv = "argv" as const; + +/** Canonical path "shell" on ExecOptions. */ +export const ExecFieldShell = "shell" as const; + +/** Canonical path "working_directory" on ExecOptions. */ +export const ExecFieldWorkingDirectory = "working_directory" as const; + +/** Canonical path "timeout" on ExecOptions. */ +export const ExecFieldTimeout = "timeout" as const; diff --git a/sdks/typescript/sandbox/src/contracts/sandbox_data_contracts.ts b/sdks/typescript/sandbox/src/contracts/sandbox_data_contracts.ts index 192dd90d..ecaaa3f6 100644 --- a/sdks/typescript/sandbox/src/contracts/sandbox_data_contracts.ts +++ b/sdks/typescript/sandbox/src/contracts/sandbox_data_contracts.ts @@ -25,6 +25,8 @@ import { type DeleteOptions as ProtoDeleteOptions, type ErrorInfo as ProtoErrorInfo, type ExitResult as ProtoExitResult, + type ExecOptions as ProtoExecOptions, + type ExecResult as ProtoExecResult, type FileCopyOptions as ProtoFileCopyOptions, type FileInfo as ProtoFileInfo, type FileReadOptions as ProtoFileReadOptions, @@ -111,6 +113,13 @@ export type ListDirectoryOptions = DurationFieldsToMs< >; export type StatOptions = DurationFieldsToMs>; export type RemoveOptions = DurationFieldsToMs>; +/** + * Public process run options for `Backend` and provider adapters (ms + * timeouts). Set exactly one of `argv` or `shell`. + */ +export type ExecOptions = DurationFieldsToMs>; +/** Outcome of `Process.run`. A non-zero `exitCode` is not an error. */ +export type ExecResult = StripProto; export type MetadataValue = StripProto; export type MetadataList = StripProto; export type MetadataObject = StripProto; diff --git a/specs/validation.yaml b/specs/validation.yaml index 9e90f197..3fe4f421 100644 --- a/specs/validation.yaml +++ b/specs/validation.yaml @@ -1,8 +1,9 @@ version: 1 -# Portable CreateOptions rules. Declared as Protovalidate options on -# proto/kit/sandbox/v1/sandbox.proto and enforced by language Protovalidate -# libraries (ValidateCreateOptions). Keep this inventory aligned with those -# annotations. Provider-specific checks stay in provider adapters / YAML. +# Portable create validation mirrors Protovalidate options on +# proto/kit/sandbox/v1/sandbox.proto (ValidateCreateOptions). ExecOptions is +# not Protovalidate: Process.Run and provider adapters enforce those gates in +# handwritten code. Keep this inventory aligned with both. Provider-specific +# create checks stay in provider adapters / YAML. messages: SandboxSource: constraints: @@ -93,6 +94,19 @@ messages: CommandProbe: fields: argv: { min_items: 1 } + # Handwritten process gates (not Protovalidate). Process.Run enforces + # exactly_one, shell empty after TrimSpace (Go) / trim() (TypeScript), + # argv min_items, and absolute working_directory. Whitespace-only shell + # fails field shell. Provider adapters enforce whole-second timeout. + ExecOptions: + fields: + argv: { min_items: 1 } + # nonblank after TrimSpace / trim(); whitespace-only is blank. + shell: { nonblank: true } + working_directory: { nonblank: true, format: absolute_path } + timeout: { minimum: 0, format: whole_seconds } + constraints: + - { op: exactly_one, fields: [argv, shell] } SecretReference: fields: reference: { nonblank: true } diff --git a/website/.gitignore b/website/.gitignore index a0c22451..4efdd6fe 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -2,6 +2,7 @@ node_modules .DS_Store .cache +.vite .vercel .output .nitro diff --git a/website/README.md b/website/README.md index b3e9fb70..5761abed 100644 --- a/website/README.md +++ b/website/README.md @@ -1,6 +1,6 @@ -# Sandbox Kit docs +# Sandbox Kit docs site -User guide for the Sandbox Kit client. The site is a Fumadocs app on TanStack Start. Pages live in `content/docs`. +User guide for the Sandbox Kit SDK—a lightweight, provider-pluggable client for cloud sandboxes. Fumadocs + TanStack Start. Source pages: `content/docs`. ```sh npm install @@ -13,8 +13,16 @@ Open http://localhost:3000. npm run build:pages ``` -That build is the GitHub Pages site. GitHub Actions publishes it from `main` to https://sandbox-kit.github.io/kit/. In the repository settings, set Pages to deploy from GitHub Actions. +GitHub Actions publishes from `main` to https://sandbox-kit.github.io/kit/. Set Pages to deploy from GitHub Actions. -TypeScript and Go share each page. The language tab is stored in the browser. Each page covers one part of the client: a provider setup, create, lifecycle, configuration, or errors. +## Structure -The behavior reference for the same API stays in the repository `docs/` directory. Runnable projects stay in `examples/`. +| Section | Pages | +| ---------- | ------------------------------------------------------------------------- | +| Quickstart | `index` | +| Concepts | `concepts` | +| Providers | `daytona`, `modal` | +| Guides | `configuration`, `create`, `lifecycle`, `filesystem`, `process`, `errors` | +| Examples | `examples` | + +Language tabs (TypeScript / Go) persist in the browser. Behavior reference for the same API lives in the repository `docs/` directory. Runnable projects live in `examples/`. diff --git a/website/content/docs/concepts.mdx b/website/content/docs/concepts.mdx new file mode 100644 index 00000000..85985927 --- /dev/null +++ b/website/content/docs/concepts.mdx @@ -0,0 +1,62 @@ +--- +title: Concepts +description: Lightweight SDK model—client, handle, pluggable providers, capabilities, close. +--- + +Sandbox Kit is a lightweight SDK over provider native clients. Depend on `@sandbox-kit/sandbox` (or the Go `sandbox` module) plus a provider module. Swap the provider; keep the same client surface. + +## Client + +Construct one client with a provider and config. Invalid config fails immediately. Credentials are not verified until the first provider request. + +| Language | Construct | +| ---------- | --------------------------------------------------- | +| TypeScript | `new Client({ provider, ... })` | +| Go | `sandbox.NewClient(provider, &sandbox.Config{...})` | + +Shared config fields: `auth`, `scope`, `endpoint`, `region`, `timeout`. See [Configuration](/docs/configuration). + +## Sandbox handle + +`create` and the provider's open method (for example Daytona `get`, Modal `attach`) return a handle. Use that handle for lifecycle, filesystem, and process calls. + +| Language | Handle API | +| ---------- | --------------------------------------------------------------------------------- | +| TypeScript | `sandbox.id`, `sandbox.filesystem`, `sandbox.process`, lifecycle methods | +| Go | `instance.ID()`, `instance.Filesystem()`, `instance.Process()`, lifecycle methods | + +## Providers + +Providers are pluggable. Each one is its own package that adapts a native SDK to the shared client. Depend only on the provider you need; add another module when you add a backend. + +| Provider | Open existing sandbox | Native SDK | +| ------------------------ | ------------------------- | -------------------- | +| [Daytona](/docs/daytona) | `get` (provider-verified) | Daytona SDK v0.225.0 | +| [Modal](/docs/modal) | `attach` (id only) | Modal SDK v0.11.1 | + +Apps construct `Client` / `NewClient` with the chosen provider. Provider modules stay outside the core SDK. + +## Capabilities + +`supports` / `Supports` reports whether the provider implements an operation. An unsupported call fails in Kit before any provider request. Daytona can still reject a supported lifecycle call for a specific sandbox; that rejection returns as a provider error. + +## Concurrency + +Calls on one handle run one at a time (lifecycle, filesystem, and process). A second handle for the same sandbox id does not serialize with the first. Abort or deadline rejects the TypeScript `await` (and ends the Go wait), but the handle stays busy until that provider call settles. + +## Close + +| Call | Effect | +| ------------------------ | ---------------------------------------- | +| Client `close` / `Close` | Closes the local provider SDK connection | +| Handle `close` / `Close` | Releases local handle resources | + +Neither call stops, terminates, archives, or deletes the cloud sandbox. You can keep using the handle after handle `close` fails only if the provider still accepts calls; prefer an explicit lifecycle method to reclaim the sandbox. + +## Errors + +Failed calls throw `SandboxError` in TypeScript and return `*sandbox.Error` in Go. Both expose `kind`, `field`, `operation`, and `message`. See [Errors](/docs/errors). + +## Out of scope + +Streaming stdin/stdout, long-lived process sessions, PTY, Daytona `code_run`, GPU requests, resizing, and browsers are not part of this client. diff --git a/website/content/docs/configuration.mdx b/website/content/docs/configuration.mdx index 40b51047..29306757 100644 --- a/website/content/docs/configuration.mdx +++ b/website/content/docs/configuration.mdx @@ -1,29 +1,32 @@ --- -title: Configure the client -description: Set auth, a timeout, a region, and provider scope when you construct the client. +title: Configuration +description: Auth, scope, endpoint, region, and default timeouts on Client construction. --- -Construct the client once, with the provider package and a config object. Later edits to that object do not change the client that is already running. Go returns `(*Client, error)`. TypeScript throws when the config is invalid. +Pass provider + config at construction. Later mutations of the config object do not affect a running client. TypeScript throws on invalid config; Go returns `(*Client, error)`. -| Field | What it does | -| ---------- | ----------------------------------------------------------------------------- | -| `provider` | The provider package, such as `daytona()` or `modal()` | -| `auth` | One credential shape. Omit it to use credentials the provider SDK already has | -| `scope` | App, environment, or organization | -| `endpoint` | Provider API URL, where that provider accepts one | -| `region` | Provider target or default placement | -| `timeout` | Default deadline for later calls | +| Field | Purpose | +| ---------- | ----------------------------------------------------------------------------------------------------- | +| `provider` | `daytona()` / `modal()` (or Go `New()`) | +| `auth` | One of `apiKey`, `tokenPair`, `bearerToken`, `oauthRefresh`. Omit to use provider SDK env credentials | +| `scope` | App, environment, or organization | +| `endpoint` | API URL when the provider allows it | +| `region` | Target / default placement | +| `timeout` | Default deadline (TS: ms, Go: `sandbox.Duration`) | -TypeScript `timeout` is milliseconds. Go `Timeout` is `sandbox.Duration`. Pass one auth shape: `apiKey`, `tokenPair`, `bearerToken`, or `oauthRefresh`. +Switch Daytona or Modal above for auth and scope samples. First client walkthrough: [Daytona](/docs/daytona) or [Modal](/docs/modal). -Build the first client on the [Daytona](/docs/daytona) or [Modal](/docs/modal) page. The sections below are the settings you change. - -## Daytona +## Client + + ```ts +import { daytona } from "@sandbox-kit/daytona"; +import { Client } from "@sandbox-kit/sandbox"; + await using client = new Client({ provider: daytona(), auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } }, @@ -32,41 +35,14 @@ await using client = new Client({ }); ``` - - - -```go -config := &sandbox.Config{ - Auth: &sandbox.AuthConfig{Credentials: &sandbox.APIKey{ - ApiKey: &sandbox.APIKeyCredentials{Key: os.Getenv("DAYTONA_API_KEY")}, - }}, - Timeout: sandbox.Duration(2 * time.Minute), -} -if target := os.Getenv("DAYTONA_TARGET"); target != "" { - config.Region = sandbox.Value(target) -} -client, err := sandbox.NewClient(daytona.New(), config) -``` - - - - -| Setting | What to pass | -| ------------ | -------------------------------------------------------------------- | -| API key | `auth.apiKey.key` / `APIKey` | -| Bearer token | `auth.bearerToken.token` / `BearerToken`, plus an organization id | -| Endpoint | `endpoint` / `Config.Endpoint`. Default `https://app.daytona.io/api` | -| Target | `region` / `Config.Region`. Leave it unset for the default target | -| Organization | `scope.organizationId` / `Scope.OrganizationId` | - -A remote endpoint uses HTTPS. HTTP is accepted for localhost and loopback. A URL with a username, password, or fragment is rejected. Sandbox Kit does not read the Daytona URL from the environment. + -## Modal - - - + ```ts +import { modal } from "@sandbox-kit/modal"; +import { Client } from "@sandbox-kit/sandbox"; + await using client = new Client({ provider: modal(), auth: { @@ -84,10 +60,47 @@ await using client = new Client({ }); ``` + + + + ```go +import ( + "os" + "time" + + "github.com/sandbox-kit/kit/sdks/go/providers/daytona" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +config := &sandbox.Config{ + Auth: &sandbox.AuthConfig{Credentials: &sandbox.APIKey{ + ApiKey: &sandbox.APIKeyCredentials{Key: os.Getenv("DAYTONA_API_KEY")}, + }}, + Timeout: sandbox.Duration(2 * time.Minute), +} +if target := os.Getenv("DAYTONA_TARGET"); target != "" { + config.Region = sandbox.Value(target) +} +client, err := sandbox.NewClient(daytona.New(), config) +``` + + + + + +```go +import ( + "os" + "time" + + "github.com/sandbox-kit/kit/sdks/go/providers/modal" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + config := &sandbox.Config{ Auth: &sandbox.AuthConfig{Credentials: &sandbox.TokenPair{ TokenPair: &sandbox.TokenPairCredentials{ @@ -107,9 +120,27 @@ if region := os.Getenv("MODAL_REGION"); region != "" { client, err := sandbox.NewClient(modal.New(), config) ``` + + + + +| Setting | What to pass | +| ------------ | -------------------------------------------------------------------- | +| API key | `auth.apiKey.key` / `APIKey` | +| Bearer token | `auth.bearerToken.token` / `BearerToken`, plus an organization id | +| Endpoint | `endpoint` / `Config.Endpoint`. Default `https://app.daytona.io/api` | +| Target | `region` / `Config.Region`. Leave it unset for the default target | +| Organization | `scope.organizationId` / `Scope.OrganizationId` | + +A remote endpoint uses HTTPS. HTTP is accepted for localhost and loopback. A URL with a username, password, or fragment is rejected. Sandbox Kit does not read the Daytona URL from the environment. + + + + + | Setting | What to pass | | ------------- | ------------------------------------------------------------------------ | | Token pair | `auth.tokenPair` / `TokenPair` | @@ -118,7 +149,9 @@ client, err := sandbox.NewClient(modal.New(), config) | Environment | `scope.environment` / `Scope.Environment`, often `main` | | Region | `region` / `Config.Region`. Leave it unset for Modal's default placement | -`endpoint` is rejected. You can still set cloud and region on a single [create](/docs/create#modal) request. +`endpoint` is rejected. You can still set cloud and region on a single [create](/docs/create) request. + + ## Timeouts @@ -126,10 +159,14 @@ client, err := sandbox.NewClient(modal.New(), config) Explicit zero adds no deadline of Sandbox Kit's own. A context deadline or an abort signal that arrives earlier still applies. -Daytona keeps its 60-second default for create, start, stop, pause, and delete when you omit the timeout, and when you pass explicit zero. A Daytona timeout that is not a whole number of seconds is rejected. + + Daytona keeps its 60-second default for create, start, stop, pause, and delete + when you omit the timeout, and when you pass explicit zero. A Daytona timeout + that is not a whole number of seconds is rejected. + ## Credentials -Keep the config object out of logs. It holds secrets. A TypeScript config is a plain object, so `JSON.stringify` includes those fields. +Do not log the config object—it contains secrets. TypeScript configs are plain objects (`JSON.stringify` includes keys). -Construction does not contact the provider. A wrong key fails on the first create, get, or attach. Read that failure in [Read an error](/docs/errors). +Construction does not contact the provider. Bad credentials fail on the first create/get/attach. See [Errors](/docs/errors). diff --git a/website/content/docs/create.mdx b/website/content/docs/create.mdx index e0e7d8cd..927503ba 100644 --- a/website/content/docs/create.mdx +++ b/website/content/docs/create.mdx @@ -1,34 +1,58 @@ --- title: Create a sandbox -description: Choose an image or a snapshot, set a size, and decide how long the sandbox lives. +description: Image or snapshot, resources, lifetime, and placement options for create. --- -`client.create` in TypeScript and `Client.Create` in Go return a handle. The handle's id is the sandbox you created. Construct the client on your provider page, or on [Configure the client](/docs/configuration). +`client.create` / `Client.Create` returns a sandbox handle. Build the client first ([Daytona](/docs/daytona), [Modal](/docs/modal), or [Configuration](/docs/configuration)). -Leave a field out to keep the provider's default. The client rejects a field that provider does not support, before it makes the call. Branch on that failure in [Read an error](/docs/errors). +Omitted fields keep provider defaults. Unsupported fields fail in Kit before the provider call ([Errors](/docs/errors)). -TypeScript durations on this request are milliseconds. A missing property means the field was omitted. Go uses `sandbox.Value` for an optional number, including zero, and `sandbox.Duration` for a timeout or a delay. +| Language | Durations | Optionals | +| ---------- | ---------------------------------------------------------------- | ------------------ | +| TypeScript | milliseconds; missing property = omitted | nested objects | +| Go | `sandbox.Duration`; numbers via `sandbox.Value` (including zero) | pointers / `Value` | ## Units -| | Daytona | Modal | -| --------- | --------------------------------------------------------------------- | ----------------------------------------------------------- | -| Source | Omit for the default snapshot, or pass a snapshot or an image | Registry image, required | -| CPU | Whole cores | Fractional cores, minimum 0.125, step 0.001 | -| Memory | Whole GiB, written as MiB (`4096` is 4 GiB) | MiB, minimum 128 when set | -| Disk | Whole GiB, written as MiB | Cannot be set | -| Lifetime | TTL, and stop, pause, archive, or delete policies | Maximum lifetime and idle termination | -| Placement | `region` on the client. A placement block on this request is rejected | `placement` on this request. Client `region` is the default | +Switch Daytona or Modal above for units and create samples. Account quotas, image names, regions, and capacity are checked by the provider. -Account quotas, image names, regions, and capacity are checked by the provider. + -## Daytona +| | Daytona | +| --------- | --------------------------------------------------------------------- | +| Source | Omit for the default snapshot, or pass a snapshot or an image | +| CPU | Whole cores | +| Memory | Whole GiB, written as MiB (`4096` is 4 GiB) | +| Disk | Whole GiB, written as MiB | +| Lifetime | TTL, and stop, pause, archive, or delete policies | +| Placement | `region` on the client. A placement block on this request is rejected | A size override requires an image. A snapshot create keeps the snapshot's size. + + + + +| | Modal | +| --------- | ----------------------------------------------------------- | +| Source | Registry image, required | +| CPU | Fractional cores, minimum 0.125, step 0.001 | +| Memory | MiB, minimum 128 when set | +| Disk | Cannot be set | +| Lifetime | Maximum lifetime and idle termination | +| Placement | `placement` on this request. Client `region` is the default | + +Pass a registry image. The app name belongs on the client, not on this request. Leave a resource out to keep Modal's default. + + + +## Example + + + ```ts const sandbox = await client.create({ source: { image: { reference: "daytonaio/playground:latest" } }, @@ -40,37 +64,9 @@ const sandbox = await client.create({ }); ``` - - + -```go -instance, err := client.Create(ctx, &sandbox.CreateOptions{ - Source: &sandbox.SandboxSource{Kind: &sandbox.Image{ - Image: &sandbox.ImageSource{Reference: "daytonaio/playground:latest"}, - }}, - Resources: &sandbox.Resources{ - CpuCores: sandbox.Value(2.0), - MemoryMib: sandbox.Value(uint64(4096)), - DiskMib: sandbox.Value(uint64(8192)), - }, -}) -``` - - - - -Name a snapshot by its reference, for example `daytona-small`. In TypeScript that is `source: { snapshot: { reference } }`. Set the toolbox language and user on `runtime`. - -A linked sandbox must be ephemeral. The client rejects the request otherwise. - -Idle stop, idle pause, stopped archive, and stopped delete use `PolicyMode` from `@sandbox-kit/sandbox` (`sandbox.PolicyModeAfter` and `sandbox.PolicyModeDisabled` in Go). A positive stop delay and a positive pause delay cannot be combined. Projects for [snapshots](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-from-snapshot), [policies](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-policies), [labels](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-labels), and [network](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-network) are in the repository. Go projects sit beside them under `examples/go/daytona/`. - -## Modal - -Pass a registry image. The app name belongs on the client, not on this request. Leave a resource out to keep Modal's default. - - - + ```ts import { PolicyMode } from "@sandbox-kit/sandbox"; @@ -91,10 +87,39 @@ const sandbox = await client.create({ `PolicyMode.After` starts the idle timer. `PolicyMode.Disabled` turns idle termination off. + + + + +```go +import "github.com/sandbox-kit/kit/sdks/go/sandbox" + +instance, err := client.Create(ctx, &sandbox.CreateOptions{ + Source: &sandbox.SandboxSource{Kind: &sandbox.Image{ + Image: &sandbox.ImageSource{Reference: "daytonaio/playground:latest"}, + }}, + Resources: &sandbox.Resources{ + CpuCores: sandbox.Value(2.0), + MemoryMib: sandbox.Value(uint64(4096)), + DiskMib: sandbox.Value(uint64(8192)), + }, +}) +``` + + + + + ```go +import ( + "time" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + instance, err := client.Create(ctx, &sandbox.CreateOptions{ Source: &sandbox.SandboxSource{Kind: &sandbox.Image{ Image: &sandbox.ImageSource{Reference: "alpine:3.21"}, @@ -117,15 +142,33 @@ instance, err := client.Create(ctx, &sandbox.CreateOptions{ }) ``` + + + + +Name a snapshot by its reference, for example `daytona-small`. In TypeScript that is `source: { snapshot: { reference } }`. Set the toolbox language and user on `runtime`. + +A linked sandbox must be ephemeral. The client rejects the request otherwise. + +Idle stop, idle pause, stopped archive, and stopped delete use `PolicyMode` from `@sandbox-kit/sandbox` (`sandbox.PolicyModeAfter` and `sandbox.PolicyModeDisabled` in Go). A positive stop delay and a positive pause delay cannot be combined. Projects for [snapshots](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-from-snapshot), [policies](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-policies), [labels](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-labels), and [network](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-network) are in the repository. Go projects sit beside them under `examples/go/daytona/`. + + + + + Set cloud and region on `placement`. A ready wait needs a probe and a positive timeout. Modal rejects a disk size, an API endpoint, and Daytona stop or archive policies. Projects for [resources](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-with-resources), [idle timeout](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-idle), [placement](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-with-placement), and [readiness](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-ready) are in the repository. Go projects sit beside them under `examples/go/modal/`. -## What you get back + + +## Result + +`info()` / `Info()` returns a **copy** of metadata. Mutating it does not change the sandbox. Reported CPU/memory can differ from requested values. -`info()` in TypeScript and `Info()` in Go return a copy of the metadata. Editing that copy does not change the sandbox. Reported CPU and memory can differ from the numbers you sent. +After create: [Filesystem](/docs/filesystem), [Run a command](/docs/process), [Lifecycle](/docs/lifecycle). -This client creates the sandbox, manages its lifetime, and works with files inside it. Commands inside the sandbox, GPU requests, resizing, extra storage, and browsers are outside it. +Not in this surface: streaming sessions, PTY, GPU, resize, extra storage, browsers. diff --git a/website/content/docs/daytona.mdx b/website/content/docs/daytona.mdx index 8d28bcc1..b963db8c 100644 --- a/website/content/docs/daytona.mdx +++ b/website/content/docs/daytona.mdx @@ -1,9 +1,9 @@ --- title: Daytona -description: Install the Daytona provider, set an API key, and create a sandbox. +description: Install, authenticate, and create a Daytona sandbox with the shared client. --- -A Daytona sandbox starts from a snapshot. Pass no create options and Sandbox Kit uses the default snapshot for your target. You need a Daytona API key. This client talks to Daytona SDK v0.225.0. +Provider package for Daytona sandboxes. Default create uses Daytona's default snapshot. Native SDK: Daytona v0.225.0. ## Install @@ -25,9 +25,9 @@ go get github.com/sandbox-kit/kit/sdks/go/providers/daytona@latest -## Create a sandbox +## Create -Set `DAYTONA_API_KEY`. This program creates one sandbox and prints its id. +Set `DAYTONA_API_KEY`. @@ -46,7 +46,7 @@ const sandbox = await client.create(); console.log(sandbox.id); ``` -`timeout` is milliseconds. `await using` closes the client when the block ends. Call `await client.close()` in a `finally` block when that syntax is unavailable. +`timeout` is milliseconds. Prefer `await using`; otherwise `await client.close()` in `finally`. @@ -84,14 +84,14 @@ func main() { } ``` -A nil create request uses the default snapshot. `Timeout` is a Go duration. +Nil create options → default snapshot. Optional strings use `sandbox.Value`. -Omit `auth` when the Daytona SDK should use credentials it already finds in the environment. Closing the client here does not delete the sandbox. +Omit `auth` to let the Daytona SDK resolve credentials from its environment. Closing the client does not delete the sandbox. -## Client settings +## Configuration | Setting | TypeScript | Go | | ------------ | ------------------------ | ---------------------- | @@ -100,16 +100,24 @@ Omit `auth` when the Daytona SDK should use credentials it already finds in the | Organization | `scope.organizationId` | `Scope.OrganizationId` | | Bearer token | `auth.bearerToken.token` | `BearerToken` | -The default API URL is `https://app.daytona.io/api`. A remote URL must be HTTPS. HTTP is accepted for localhost and loopback. Sandbox Kit does not read the URL from the environment. A bearer token requires an organization id. +Default API URL: `https://app.daytona.io/api`. Remote URLs require HTTPS. HTTP is allowed for localhost/loopback. Bearer tokens require an organization id. Full rules: [Configuration](/docs/configuration). -In Go, set an optional string with `sandbox.Value`. Full field rules are on [Configure the client](/docs/configuration). +## Operations -## Where to go next +| Area | Support | +| --------- | ----------------------------------------------------------------- | +| Create | Default snapshot, named snapshot, or image (+ resources) | +| Open | `get` (verified). `attach` unsupported | +| Lifecycle | `refresh`, `start`, `stop`, `pause`, `archive`, `delete`, `close` | +| Files | Full filesystem API — [Filesystem](/docs/filesystem) | +| Process | `run` — [Run a command](/docs/process) | -| You want to | Open | -| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| An image, a named snapshot, a size, or a lifetime | [Create a sandbox](/docs/create#daytona) | -| Look up, stop, or delete a sandbox | [Manage a sandbox](/docs/lifecycle#open-a-sandbox) | -| Work with files | [Use files](/docs/filesystem) | -| A failed call | [Read an error](/docs/errors) | -| A project you can run | [create-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-sandbox) and the [Go project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-sandbox) | +CPU: whole cores. Memory/disk: whole GiB as MiB. Resource overrides require an image source. + +## Related + +| Topic | Link | +| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Image, size, policies | [Create](/docs/create#daytona) | +| get / stop / delete | [Lifecycle](/docs/lifecycle) | +| Example project | [create-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-sandbox) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-sandbox) | diff --git a/website/content/docs/errors.mdx b/website/content/docs/errors.mdx index fb15d577..f50686e4 100644 --- a/website/content/docs/errors.mdx +++ b/website/content/docs/errors.mdx @@ -1,11 +1,11 @@ --- -title: Read an error -description: Branch on the error kind. The same kinds come back from every provider. +title: Errors +description: SandboxError / *sandbox.Error kinds, fields, and provider causes. --- -A failed call throws in TypeScript and returns an error in Go. Both carry a kind, the provider, the operation, and, when a setting was rejected, the field that failed. +TypeScript throws `SandboxError`. Go returns `*sandbox.Error`. Both include `kind`, `provider`, `operation`, `message`, and `field` when a setting was rejected. -Branch on the kind. The same list applies to every provider. The kind names a category. It does not mean the call is safe to retry. Sandbox Kit does not retry for you. +Kinds are shared across providers. A kind is a category—not a retry policy. Kit does not retry. | Kind | What happened | | ------------------- | ------------------------------------------------------ | @@ -19,7 +19,7 @@ Branch on the kind. The same list applies to every provider. The kind names a ca | `Canceled` | The caller canceled the operation | | `Timeout` | The deadline fired | -## Branch on the kind +## Handle by kind @@ -70,6 +70,13 @@ The provider's own error is `error.cause`. ```go +import ( + "errors" + "fmt" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + var detail *sandbox.Error if errors.As(err, &detail) { switch detail.Kind { @@ -98,16 +105,14 @@ if errors.As(err, &detail) { -## Read the field - -Go and TypeScript use the same field string. A rule about one setting names that setting, such as `resources.cpu_cores`. A rule about several settings names their parent, such as `network`. +## Field paths -In TypeScript, compare `error.field` with the constants exported from `@sandbox-kit/sandbox`, such as `CreateFieldResourcesCpuCores`. +Field strings match across languages (for example `resources.cpu_cores`). Rules that cover several settings use the parent (`network`). TypeScript exports constants such as `CreateFieldResourcesCpuCores` for comparisons. -## After a failure +## After failure -An error does not prove that no sandbox exists. The provider may have created one before a later step failed, and the error does not include that id. A lifecycle call can fail after the provider has already changed the sandbox. +An error does not prove no sandbox was created—the provider may have succeeded before a later step failed, and the error does not include that id. Lifecycle failures can occur after the provider already mutated state. -On Daytona, look the sandbox up with `get` before you retry. Modal `attach` does not perform that lookup. +On Daytona, `get` before retry. Modal `attach` does not verify existence. -Runnable projects: [Daytona errors](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/errors/handle-daytona-errors) and [Modal errors](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/errors/handle-modal-errors). The Go projects sit beside them under `examples/go/`. +Examples: [Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/errors/handle-daytona-errors) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/errors/handle-modal-errors) · Go twins under `examples/go/`. diff --git a/website/content/docs/examples.mdx b/website/content/docs/examples.mdx index 33e5bb29..72c280b3 100644 --- a/website/content/docs/examples.mdx +++ b/website/content/docs/examples.mdx @@ -1,42 +1,52 @@ --- title: Examples -description: Run one project for one operation. Each project loads its own environment file. +description: One runnable project per operation. Clone, set .env, run. --- -Each project selects one provider, loads a `.env` file, and performs one operation. Open the project for the call you need. +Each example is a small project under `examples////`. It loads its own `.env` and demonstrates one call. -Creation projects provision a real sandbox. Filesystem and lifecycle projects use the sandbox id in `.env`. Stop, terminate, archive, delete, and remove can destroy that sandbox or the path you set. Error projects fail locally and create nothing. +| Area | Runtime | +| ------------------------------------ | ----------------------------- | +| `configuration` | Provisions a real sandbox | +| `filesystem`, `process`, `lifecycle` | Uses `*_SANDBOX_ID` in `.env` | +| `errors` | Fails locally; no cloud work | -The tables below are the common projects. The repository also has network, labels, secrets, pause, archive, and a project for each filesystem call (read, write, copy, mkdir, list, stat, remove). The full list is the [examples catalog](https://github.com/sandbox-kit/kit/blob/main/examples/README.md). +Destructive ops (stop, terminate, archive, delete, remove) can destroy the sandbox or path you configure. Full catalog: [examples/README.md](https://github.com/sandbox-kit/kit/blob/main/examples/README.md). ## Daytona -| Operation | TypeScript | Go | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -| Create | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-sandbox) | -| Read a file | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/read-daytona-file) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/read-daytona-file) | -| From an image | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-from-image) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-from-image) | -| From a snapshot | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-from-snapshot) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-from-snapshot) | -| Lifetime policies | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-policies) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-with-policies) | -| Look up | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/lifecycle/get-daytona-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/lifecycle/get-daytona-sandbox) | -| Stop | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/lifecycle/stop-daytona-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/lifecycle/stop-daytona-sandbox) | -| Delete | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/lifecycle/delete-daytona-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/lifecycle/delete-daytona-sandbox) | -| Errors | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/errors/handle-daytona-errors) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/errors/handle-daytona-errors) | +| Operation | TypeScript | Go | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| Create | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-sandbox) | +| Run command | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process/run-daytona-command) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process/run-daytona-command) | +| Read file | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/read-daytona-file) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/read-daytona-file) | +| From image | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-from-image) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-from-image) | +| From snapshot | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-from-snapshot) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-from-snapshot) | +| Policies | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/configuration/create-daytona-with-policies) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/configuration/create-daytona-with-policies) | +| Get | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/lifecycle/get-daytona-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/lifecycle/get-daytona-sandbox) | +| Stop | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/lifecycle/stop-daytona-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/lifecycle/stop-daytona-sandbox) | +| Delete | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/lifecycle/delete-daytona-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/lifecycle/delete-daytona-sandbox) | +| Errors | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/errors/handle-daytona-errors) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/errors/handle-daytona-errors) | + +Process variants (shell, cwd, env, timeout, exit): [typescript/daytona/process](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process) · [go/daytona/process](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process). ## Modal | Operation | TypeScript | Go | | ----------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | Create | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/configuration/create-modal-sandbox) | -| Read a file | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/read-modal-file) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/read-modal-file) | +| Run command | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process/run-modal-command) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process/run-modal-command) | +| Read file | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/read-modal-file) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/read-modal-file) | | Resources | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-with-resources) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/configuration/create-modal-with-resources) | | Attach | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/lifecycle/attach-modal-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/lifecycle/attach-modal-sandbox) | | Terminate | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/lifecycle/terminate-modal-sandbox) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/lifecycle/terminate-modal-sandbox) | | Errors | [project](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/errors/handle-modal-errors) | [project](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/errors/handle-modal-errors) | -## Run a project +Process variants: [typescript/modal/process](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process) · [go/modal/process](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process). + +## Run -TypeScript needs Node.js 20, 22, or 24.15+. Go needs Go 1.26.0 or later. `` is `filesystem`, `configuration`, `lifecycle`, or `errors`. Replace the path with the project you opened above. Skip `cp .env.example` when you already have a `.env`. +Requirements: Node.js 20/22/24.15+ (npm 11.19.0) or Go 1.26.0+. `` is `filesystem`, `process`, `configuration`, `lifecycle`, or `errors`. Skip `cp` if `.env` exists. @@ -61,4 +71,4 @@ go run . -Put the variables from that project's `.env.example` into `.env`. Daytona create uses `DAYTONA_API_KEY`. Modal create uses `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET`, and `MODAL_APP_NAME`. +Daytona create needs `DAYTONA_API_KEY`. Modal create needs `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET`, and `MODAL_APP_NAME`. diff --git a/website/content/docs/filesystem.mdx b/website/content/docs/filesystem.mdx index 93b38934..91bd8124 100644 --- a/website/content/docs/filesystem.mdx +++ b/website/content/docs/filesystem.mdx @@ -1,30 +1,63 @@ --- -title: Use files -description: Read, write, copy, mkdir, list, stat, and remove files inside a sandbox. +title: Filesystem +description: Read, write, copy, mkdir, list, stat, and remove paths inside a sandbox. --- -File calls live on the handle you already have. Daytona and Modal implement the same methods. Create the sandbox on the provider page, or open one you already have from [Manage a sandbox](/docs/lifecycle). +Use `sandbox.filesystem` / `Filesystem()` on an open handle. Providers implement the same methods; provider-specific timeout rules are below. -| You want to | Call | -| ----------------------------- | --------------------------------------- | -| Read the bytes of one file | [`readFile`](#read-a-file) | -| Replace one file | [`writeFile`](#write-a-file) | -| Copy a file from your machine | [`copyFromLocal`](#copy-a-file-in) | -| Copy a file onto your machine | [`copyToLocal`](#copy-a-file-out) | -| Create a directory | [`makeDirectory`](#create-a-directory) | -| List one directory | [`listDirectory`](#list-a-directory) | -| Read metadata for one path | [`stat`](#read-metadata) | -| Delete a file or directory | [`remove`](#delete-a-file-or-directory) | +| Method | Purpose | +| --------------------------------------- | ------------------------ | +| [`readFile`](#read-a-file) | Read file bytes | +| [`writeFile`](#write-a-file) | Replace or create a file | +| [`copyFromLocal`](#copy-a-file-in) | Local → sandbox | +| [`copyToLocal`](#copy-a-file-out) | Sandbox → local | +| [`makeDirectory`](#create-a-directory) | Create a directory | +| [`listDirectory`](#list-a-directory) | List one directory | +| [`stat`](#read-metadata) | Path metadata | +| [`remove`](#delete-a-file-or-directory) | Delete a path | -A path inside the sandbox starts with `/`, such as `/tmp/note.txt`. A relative path, a blank path, or a path that contains a NUL byte is rejected before the provider is called, and the error names the field `path`. A path on your machine may be relative to the process, such as `./note.txt`. It must be non-blank and must not contain a NUL byte. A rejected local path names the field `local_path`. +Prerequisite: [create](/docs/create) or [open](/docs/lifecycle) a sandbox. -`readFile` and `writeFile` move bytes. Encode text before you write it, and decode text after you read it. `copyFromLocal` and `copyToLocal` use files on your disk. +Calls on one handle run one at a time with lifecycle and process. Abort or +deadline rejects the TypeScript `await` (and ends the Go wait), but the handle +stays busy until that provider call settles. -Leave `timeout` unset to use the client deadline. Set `timeout: 0` when this call should add no deadline of its own. TypeScript records the timeout in milliseconds. Go records it as a `time.Duration`. +## Paths and bytes -TypeScript Daytona `readFile`, `writeFile`, `copyFromLocal`, and `copyToLocal` pass that duration to Daytona as a whole number of seconds. `2_000` is two seconds. `1_500` is 1.5 seconds, so those four calls reject it. The error field is `timeout`. An omitted timeout, and an explicit zero, on those four calls are passed as `0`. Daytona adds no transfer limit. When the timeout is omitted, the abort signal carries the client deadline. +| Path kind | Rule | Error field | +| ------------ | ------------------------------------------------------------ | ------------ | +| Sandbox | Absolute (`/tmp/note.txt`). Relative, blank, or NUL rejected | `path` | +| Local (copy) | May be relative (`./note.txt`). Non-blank, no NUL | `local_path` | -`makeDirectory`, `listDirectory`, `stat`, and `remove` use the abort signal. `1_500` ends the call at 1.5 seconds. Every Go file call uses the context as the deadline, so `1500 * time.Millisecond` ends the call at 1.5 seconds. Modal file calls use the context or the abort signal. `1_500` milliseconds ends the call at 1.5 seconds. +`readFile` / `writeFile` transfer bytes—encode/decode text in your app. Copy methods use files on disk. + +## Timeouts + +| Language | Unit | +| ---------- | --------------- | +| TypeScript | milliseconds | +| Go | `time.Duration` | + +Omit timeout → client deadline. Explicit `0` → no extra Kit deadline. + + + +| Method / language | Behavior | +| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| TS `readFile`, `writeFile`, `copyFromLocal`, `copyToLocal` | Whole seconds to Daytona. `2000` OK; `1500` rejected (`timeout`). Omit/zero → `0` (no transfer limit). Abort signal still applies when omit | +| TS `makeDirectory`, `listDirectory`, `stat`, `remove` | Abort signal. `1500` ms ends at 1.5s | +| All Go file calls | Context deadline. `1500 * time.Millisecond` ends at 1.5s | + + + + + +| Method / language | Behavior | +| ----------------------------------------------------- | -------------------------------------------------------- | +| TS transfer and directory ops | Abort signal. `1500` ms ends at 1.5s | +| All Go file calls | Context deadline. `1500 * time.Millisecond` ends at 1.5s | + + @@ -34,10 +67,20 @@ await sandbox.filesystem.readFile("/tmp/note.txt", { timeout: 2_000 }); await sandbox.filesystem.listDirectory("/tmp", { timeout: 1_500 }); ``` + +`readFile` with `1500` is rejected on Daytona TypeScript. Use a whole-second value such as `2000`. + + ```go +import ( + "time" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + data, err := instance.Filesystem().ReadFile(ctx, "/tmp/note.txt", &sandbox.FileReadOptions{ Timeout: sandbox.Duration(1500 * time.Millisecond), }) @@ -46,9 +89,7 @@ data, err := instance.Filesystem().ReadFile(ctx, "/tmp/note.txt", &sandbox.FileR -Calls on one handle run one at a time, including lifecycle calls. A second handle for the same sandbox does not wait. - -The snippets below assume you already have `sandbox` or `instance`. Each call also has its own project in the [examples](/docs/examples) catalog. +Calls on one handle are serialized with lifecycle and process. Snippets below assume an open `sandbox` / `instance`. See [Examples](/docs/examples). ## Read a file @@ -67,6 +108,8 @@ console.log(text); ```go +import "fmt" + data, err := instance.Filesystem().ReadFile(ctx, "/tmp/note.txt", nil) if err != nil { return err @@ -77,7 +120,13 @@ fmt.Printf("%s\n", data) -[Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/read-daytona-file) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/read-modal-file). Go: [Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/read-daytona-file) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/read-modal-file). + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/read-daytona-file) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/read-daytona-file) + + + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/read-modal-file) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/read-modal-file) + ## Write a file @@ -102,7 +151,13 @@ err := instance.Filesystem().WriteFile(ctx, "/tmp/kit-files/note.txt", body, nil -[Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/write-daytona-file) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/write-modal-file). Go: [Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/write-daytona-file) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/write-modal-file). + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/write-daytona-file) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/write-daytona-file) + + + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/write-modal-file) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/write-modal-file) + ## Copy a file in @@ -125,7 +180,13 @@ err := instance.Filesystem().CopyFromLocal(ctx, "note.txt", "/tmp/note.txt", nil -[Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/copy-file-into-daytona) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/copy-file-into-modal). Go: [Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/copy-file-into-daytona) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/copy-file-into-modal). + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/copy-file-into-daytona) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/copy-file-into-daytona) + + + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/copy-file-into-modal) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/copy-file-into-modal) + ## Copy a file out @@ -148,38 +209,72 @@ err := instance.Filesystem().CopyToLocal(ctx, "/tmp/note.txt", "note.txt", nil) -[Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/copy-file-from-daytona) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/copy-file-from-modal). Go: [Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/copy-file-from-daytona) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/copy-file-from-modal). + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/copy-file-from-daytona) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/copy-file-from-daytona) + + + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/copy-file-from-modal) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/copy-file-from-modal) + ## Create a directory -`parents` defaults to true, so the call creates missing parents. Modal accepts a directory that already exists. Daytona returns an error when it already exists. +`parents` defaults to true, so the call creates missing parents. + + +Daytona returns an error when the directory already exists. Daytona rejects `parents: false` before the call (field `parents`). + -Set `parents` to false when the parent directory must already exist. Modal then requires that parent, and returns an error if the new directory is already there. Daytona rejects `false` before the call. The error field is `parents`. + +Modal accepts a directory that already exists. With `parents: false`, Modal requires the parent and errors if the new directory is already there. + ```ts await sandbox.filesystem.makeDirectory("/tmp/kit-files"); +``` + + + +```ts await sandbox.filesystem.makeDirectory("/tmp/kit-files/nested", { parents: false, }); ``` + + ```go err := instance.Filesystem().MakeDirectory(ctx, "/tmp/kit-files", nil) +``` + + + +```go +import "github.com/sandbox-kit/kit/sdks/go/sandbox" + err = instance.Filesystem().MakeDirectory(ctx, "/tmp/kit-files/nested", &sandbox.MakeDirectoryOptions{ Parents: sandbox.Value(false), }) ``` + + -[Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/make-daytona-directory) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/make-modal-directory). Go: [Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/make-daytona-directory) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/make-modal-directory). + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/make-daytona-directory) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/make-daytona-directory) + + + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/make-modal-directory) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/make-modal-directory) + ## List a directory @@ -197,6 +292,8 @@ for (const entry of entries) console.log(entry.name, entry.size); ```go +import "fmt" + entries, err := instance.Filesystem().ListDirectory(ctx, "/tmp/kit-files", nil) if err != nil { return err @@ -209,7 +306,13 @@ for _, entry := range entries { -[Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/list-daytona-directory) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/list-modal-directory). Go: [Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/list-daytona-directory) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/list-modal-directory). + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/list-daytona-directory) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/list-daytona-directory) + + + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/list-modal-directory) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/list-modal-directory) + ## Read metadata @@ -227,6 +330,8 @@ console.log(info.type, info.size, info.mode); ```go +import "fmt" + info, err := instance.Filesystem().Stat(ctx, "/tmp/note.txt", nil) if err != nil { return err @@ -237,7 +342,13 @@ fmt.Println(info.GetType(), info.GetSize(), info.GetMode()) -[Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/stat-daytona-file) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/stat-modal-file). Go: [Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/stat-daytona-file) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/stat-modal-file). + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/stat-daytona-file) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/stat-daytona-file) + + + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/stat-modal-file) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/stat-modal-file) + ## Delete a file or directory @@ -255,6 +366,8 @@ await sandbox.filesystem.remove("/tmp/kit-files", { recursive: true }); ```go +import "github.com/sandbox-kit/kit/sdks/go/sandbox" + files := instance.Filesystem() if err := files.Remove(ctx, "/tmp/note.txt", nil); err != nil { return err @@ -269,4 +382,10 @@ if err := files.Remove(ctx, "/tmp/kit-files", &sandbox.RemoveOptions{ -[Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/remove-daytona-file) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/remove-modal-file). Go: [Daytona](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/remove-daytona-file) · [Modal](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/remove-modal-file). + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/filesystem/remove-daytona-file) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/filesystem/remove-daytona-file) + + + +Examples: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/filesystem/remove-modal-file) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/filesystem/remove-modal-file) + diff --git a/website/content/docs/index.mdx b/website/content/docs/index.mdx index 567ce25e..802e54c5 100644 --- a/website/content/docs/index.mdx +++ b/website/content/docs/index.mdx @@ -1,30 +1,298 @@ --- -title: Get started -description: Open the page for the call you need. Each page stands on its own. +title: Quickstart +description: Install Sandbox Kit, create a sandbox, run a command, and write a file. --- -Sandbox Kit is one client for the sandbox provider you pass in. Install the core package and that provider's package. An agent uses the client to open a sandbox and control how long it runs. +Sandbox Kit is a lightweight TypeScript and Go SDK for cloud sandboxes. The core package defines the client; providers plug in as separate packages. Install both, construct a client, then create or open sandboxes. -Open the page that matches the call. You do not need the other pages first. +| Requirement | Version | +| ----------- | ------------------------- | +| TypeScript | Node.js 20, 22, or 24.15+ | +| Go | Go 1.26.0+ | -| You want to | Open | -| --------------------------------------------- | ------------------------------------------------ | -| Install a provider and create a sandbox | [Daytona](/docs/daytona) or [Modal](/docs/modal) | -| Set auth, a timeout, a region, or an app | [Configure the client](/docs/configuration) | -| Choose an image, a size, or a lifetime | [Create a sandbox](/docs/create) | -| Open a sandbox you already have, or finish it | [Manage a sandbox](/docs/lifecycle) | -| Work with files inside the sandbox | [Use files](/docs/filesystem) | -| Branch on a failed call | [Read an error](/docs/errors) | -| Copy a runnable project | [Examples](/docs/examples) | +Language tabs and the Daytona / Modal menu keep your picks in the browser. The menu switches install snippets, create samples, and provider notes. -Code samples have a **TypeScript** tab and a **Go** tab. The choice is saved in this browser. +## 1. Install -TypeScript needs Node.js 20, 22, or 24.15+. Go needs Go 1.26.0 or later. +Install the core package and a provider. - - `client.close()` and `Client.Close` release the connection in your process. - The sandbox keeps running. Finish it from [Manage a sandbox](/docs/lifecycle), - or set a lifetime when you [create](/docs/create) it. + + + + + +```sh +npm install @sandbox-kit/sandbox @sandbox-kit/daytona +``` + + + + + +```sh +npm install @sandbox-kit/sandbox @sandbox-kit/modal +``` + + + + + + + + +```sh +go get github.com/sandbox-kit/kit/sdks/go/sandbox@latest +go get github.com/sandbox-kit/kit/sdks/go/providers/daytona@latest +``` + + + + + +```sh +go get github.com/sandbox-kit/kit/sdks/go/sandbox@latest +go get github.com/sandbox-kit/kit/sdks/go/providers/modal@latest +``` + + + + + + +## 2. Create a sandbox + +Set `DAYTONA_API_KEY`, then run: + + + Set a Modal token pair and app name (see [Modal](/docs/modal)), then run: + + + + + + + +```ts +import { daytona } from "@sandbox-kit/daytona"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: daytona(), + auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } }, + timeout: 120_000, +}); + +const sandbox = await client.create(); +console.log(sandbox.id); +``` + + + + + +```ts +import { modal } from "@sandbox-kit/modal"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: modal(), + auth: { + tokenPair: { + id: process.env.MODAL_TOKEN_ID!, + secret: process.env.MODAL_TOKEN_SECRET!, + }, + }, + scope: { + appName: process.env.MODAL_APP_NAME!, + environment: process.env.MODAL_ENVIRONMENT ?? "main", + }, + timeout: 120_000, +}); + +const sandbox = await client.create({ + source: { image: { reference: "alpine:3.21" } }, +}); +console.log(sandbox.id); +``` + + + + + + + + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/sandbox-kit/kit/sdks/go/providers/daytona" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + client, err := sandbox.NewClient(daytona.New(), &sandbox.Config{ + Auth: &sandbox.AuthConfig{Credentials: &sandbox.APIKey{ + ApiKey: &sandbox.APIKeyCredentials{Key: os.Getenv("DAYTONA_API_KEY")}, + }}, + Timeout: sandbox.Duration(2 * time.Minute), + }) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + + instance, err := client.Create(context.Background(), nil) + if err != nil { + log.Fatal(err) + } + fmt.Println(instance.ID()) +} +``` + + + + + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/sandbox-kit/kit/sdks/go/providers/modal" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +func main() { + client, err := sandbox.NewClient(modal.New(), &sandbox.Config{ + Auth: &sandbox.AuthConfig{Credentials: &sandbox.TokenPair{ + TokenPair: &sandbox.TokenPairCredentials{ + Id: os.Getenv("MODAL_TOKEN_ID"), + Secret: os.Getenv("MODAL_TOKEN_SECRET"), + }, + }}, + Scope: &sandbox.Scope{ + AppName: sandbox.Value(os.Getenv("MODAL_APP_NAME")), + Environment: sandbox.Value("main"), + }, + Timeout: sandbox.Duration(2 * time.Minute), + }) + if err != nil { + log.Fatal(err) + } + defer client.Close(context.Background()) + + instance, err := client.Create(context.Background(), &sandbox.CreateOptions{ + Source: &sandbox.SandboxSource{Kind: &sandbox.Image{ + Image: &sandbox.ImageSource{Reference: "alpine:3.21"}, + }}, + }) + if err != nil { + log.Fatal(err) + } + fmt.Println(instance.ID()) +} +``` + + + + + + +Provider setup details: [Daytona](/docs/daytona) · [Modal](/docs/modal). + +## 3. Run a command + + + + +```ts +const result = await sandbox.process.run({ + argv: ["echo", "hello from kit"], +}); +console.log(result.exitCode, new TextDecoder().decode(result.stdout)); +``` + + + + +```go +import ( + "context" + "fmt" + "log" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +result, err := instance.Process().Run(context.Background(), &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, +}) +if err != nil { + log.Fatal(err) +} +fmt.Println(result.GetExitCode(), string(result.GetStdout())) +``` + + + + +A non-zero exit code is not an SDK error. Check `exitCode` / `exit_code` yourself. Details: [Run a command](/docs/process). + +## 4. Write a file + + + + +```ts +await sandbox.filesystem.writeFile( + "/tmp/note.txt", + new TextEncoder().encode("hello\n"), +); +``` + + + + +```go +import "context" + +err = instance.Filesystem().WriteFile( + context.Background(), + "/tmp/note.txt", + []byte("hello\n"), + nil, +) +``` + + + + +Paths inside the sandbox are absolute. Details: [Filesystem](/docs/filesystem). + + + `client.close()` / `Client.Close` and handle `close` release local resources + only. The cloud sandbox keeps running until you stop, terminate, archive, + delete, or a lifetime policy fires. See [Concepts](/docs/concepts). -A client that constructs successfully has not checked your credentials. The provider checks them on the first request. +## Next + +| Task | Page | +| ----------------------------------- | ------------------------------------ | +| Client, handle, and close semantics | [Concepts](/docs/concepts) | +| Daytona setup | [Daytona](/docs/daytona) | +| Modal setup | [Modal](/docs/modal) | +| Auth, timeout, region, scope | [Configuration](/docs/configuration) | +| Runnable projects | [Examples](/docs/examples) | diff --git a/website/content/docs/lifecycle.mdx b/website/content/docs/lifecycle.mdx index 597bcf43..79af6d54 100644 --- a/website/content/docs/lifecycle.mdx +++ b/website/content/docs/lifecycle.mdx @@ -1,41 +1,69 @@ --- -title: Manage a sandbox -description: Open a sandbox you already have, stop or terminate it, and close your local connection. +title: Lifecycle +description: get, attach, start, stop, pause, archive, delete, terminate, and close. --- -Create returns a handle. Later calls use that handle, or a handle you open from an id. Pick the row for the call you need. +Operate on a handle from `create`, or open one by id. Switch Daytona or Modal above for open, destroy, and capability notes. -| You want to | Daytona | Modal | -| --------------------------- | ------------------------------------------ | ---------------------------------------------------------------- | -| Open an existing sandbox | [`get`](#open-a-sandbox) looks the id up | [`attach`](#open-a-sandbox) takes the id and does not look it up | -| Start, stop, pause, archive | [Supported](#stop-start-pause-and-archive) | Rejected before the call | -| Remove the sandbox | [`delete`](#delete) | Use [`terminate`](#terminate), or a lifetime set at create time | -| Drop your local connection | [`close`](#close-the-client) | [`close`](#close-the-client) | +| Operation | Daytona | Modal | +| ------------------------------ | ------------------------------------------ | ------------------------------------------------- | +| Open existing | [`get`](#open-a-sandbox) (provider lookup) | [`attach`](#open-a-sandbox) (id only, no lookup) | +| start / stop / pause / archive | [Supported](#stop-start-pause-and-archive) | Rejected in Kit | +| Destroy | [`delete`](#delete) | [`terminate`](#terminate) or create-time lifetime | +| Local disconnect | [`close`](#close-the-client) | [`close`](#close-the-client) | -`supports` reports whether this provider implements the call. Import `LifecycleCapability` from `@sandbox-kit/sandbox`. In Go the constants are `sandbox.LifecycleCapabilityStop` and the rest of that set. A missing operation fails before the provider is contacted. Daytona can still reject a supported call for one sandbox, and that error comes back to you. +Check support with `supports` / `Supports` (`LifecycleCapability*` in Go). Unsupported ops fail before the provider call. Daytona may still reject a supported op for a given sandbox. -The snippets below assume you already have a `client`. Build one on the provider page. +Calls on one handle run one at a time with filesystem and process. Abort or +deadline rejects the TypeScript `await` (and ends the Go wait), but the handle +stays busy until that provider call settles. + +Snippets assume a constructed `client` ([Daytona](/docs/daytona) / [Modal](/docs/modal)). ## Open a sandbox - - + + +`get` loads a sandbox that already exists. `refresh` loads it again. `Attach` is not available. + + + + -`get` loads a sandbox that already exists. `refresh` loads it again. +`attach` builds a handle from an id. It does not ask Modal whether that sandbox exists. Modal has no `get` and no `refresh`. + + + + ```ts const sandbox = await client.get(process.env.DAYTONA_SANDBOX_ID!); const info = await sandbox.refresh(); console.log(sandbox.id, info.state); ``` + + + + +```ts +const sandbox = await client.attach(process.env.MODAL_SANDBOX_ID!); +console.log(sandbox.id); +``` + + + + + ```go +import "os" + instance, err := client.Get(ctx, os.Getenv("DAYTONA_SANDBOX_ID")) if err != nil { return err @@ -45,48 +73,36 @@ if err := instance.Refresh(ctx); err != nil { } ``` -`Attach` is not available. - - - - - - - -`attach` builds a handle from an id. It does not ask Modal whether that sandbox exists. Modal has no `get` and no `refresh`. + - - - -```ts -const sandbox = await client.attach(process.env.MODAL_SANDBOX_ID!); -console.log(sandbox.id); -``` - - - + ```go +import "os" + instance, err := client.Attach(ctx, os.Getenv("MODAL_SANDBOX_ID")) if err != nil { return err } ``` - - + ## Stop, start, pause, and archive -These four calls are Daytona calls. Modal rejects them. `stop` asks Daytona to stop the sandbox. The sandbox stays available to start again. `start` brings a stopped or archived sandbox back. `pause` keeps memory and stops the sandbox. `archive` moves a retained sandbox into archive storage. + + +These four calls are Daytona calls. `stop` asks Daytona to stop the sandbox. The sandbox stays available to start again. `start` brings a stopped or archived sandbox back. `pause` keeps memory and stops the sandbox. `archive` moves a retained sandbox into archive storage. ```ts +import { LifecycleCapability } from "@sandbox-kit/sandbox"; + const sandbox = await client.get(process.env.DAYTONA_SANDBOX_ID!); if (!sandbox.supports(LifecycleCapability.Stop)) { @@ -103,6 +119,14 @@ console.log(sandbox.id, info.state); ```go +import ( + "fmt" + "os" + "time" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + instance, err := client.Get(ctx, os.Getenv("DAYTONA_SANDBOX_ID")) if err != nil { return err @@ -122,11 +146,21 @@ if err := instance.Stop(ctx, &sandbox.StopOptions{ -Daytona decides whether that sandbox can pause, archive, or stop. An ephemeral sandbox can still be removed by the lifetime you set at [create](/docs/create#daytona) time. +Daytona decides whether that sandbox can pause, archive, or stop. An ephemeral sandbox can still be removed by the lifetime you set at [create](/docs/create) time. + + + + + +Modal rejects `stop`, `start`, `pause`, and `archive` in Kit. Use [terminate](#terminate) or create-time lifetime policies instead. + + ## Delete -`delete` removes a Daytona sandbox. Modal rejects it. + + +`delete` removes a Daytona sandbox. @@ -140,6 +174,13 @@ await sandbox.delete({ timeout: 60_000 }); ```go +import ( + "os" + "time" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + instance, err := client.Get(ctx, os.Getenv("DAYTONA_SANDBOX_ID")) if err != nil { return err @@ -154,14 +195,26 @@ if err := instance.Delete(ctx, &sandbox.DeleteOptions{ + + + + +Modal rejects `delete`. Use [terminate](#terminate) instead. + + + ## Terminate -`terminate` finishes a Modal sandbox. It is not a stop, and it is not a delete. Daytona rejects it. A maximum lifetime or an idle timeout set at [create](/docs/create#modal) time also ends the sandbox. + + +`terminate` finishes a Modal sandbox. It is not a stop, and it is not a delete. A maximum lifetime or an idle timeout set at [create](/docs/create) time also ends the sandbox. ```ts +import { LifecycleCapability } from "@sandbox-kit/sandbox"; + const sandbox = await client.attach(process.env.MODAL_SANDBOX_ID!); if (!sandbox.supports(LifecycleCapability.Terminate)) { @@ -178,6 +231,14 @@ console.log(sandbox.id, exit.exitCode); ```go +import ( + "fmt" + "os" + "time" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + instance, err := client.Attach(ctx, os.Getenv("MODAL_SANDBOX_ID")) if err != nil { return err @@ -198,6 +259,14 @@ fmt.Println(exit.GetExitCode()) + + + + +Daytona rejects `terminate`. Use [delete](#delete) or create-time lifetime policies instead. + + + ## Close the client @@ -207,20 +276,22 @@ fmt.Println(exit.GetExitCode()) await client.close(); ``` -Prefer `await using` when the client's lifetime matches the block. Disposing the client closes the connection. Disposing the handle leaves the sandbox as it is. +Prefer `await using` when the client lifetime matches the block. Client dispose closes the connection. Handle dispose does not stop the sandbox. ```go +import "context" + defer client.Close(context.Background()) ``` -`Sandbox.Close` does not change the sandbox. A later call on that handle still reaches the provider. +`Sandbox.Close` does not stop or delete the sandbox. Later calls on that handle still reach the provider. ## Read the state -`info.state` is the portable state. The provider's own string is kept beside it. After a lifecycle call, read the handle again before you treat the sandbox as stopped or gone. A failed call can mean the provider already changed it. See [Read an error](/docs/errors). +`info.state` is the portable state; the provider's native string sits beside it. After a lifecycle call, refresh or re-fetch before assuming the sandbox is stopped or gone. A failed call can still mean the provider already changed state. See [Errors](/docs/errors). diff --git a/website/content/docs/meta.json b/website/content/docs/meta.json index 1cd42cca..d9a7128d 100644 --- a/website/content/docs/meta.json +++ b/website/content/docs/meta.json @@ -1,16 +1,19 @@ { - "title": "Guides", + "title": "Documentation", "pages": [ "index", + "concepts", "---Providers---", "daytona", "modal", - "---Use the client---", + "---Guides---", + "configuration", "create", "lifecycle", "filesystem", - "configuration", + "process", "errors", + "---Examples---", "examples" ] } diff --git a/website/content/docs/modal.mdx b/website/content/docs/modal.mdx index 2d483b84..0f9ca39c 100644 --- a/website/content/docs/modal.mdx +++ b/website/content/docs/modal.mdx @@ -1,9 +1,9 @@ --- title: Modal -description: Install the Modal provider, set a token pair and an app name, and create a sandbox from an image. +description: Install, authenticate, and create a Modal sandbox from a registry image. --- -A Modal sandbox starts from a container image. You need a Modal token pair and an app that already exists. This client talks to Modal SDK v0.11.1. +Provider package for Modal sandboxes. Create requires a registry image, a token pair, and an existing Modal app. Native SDK: Modal v0.11.1. ## Install @@ -25,9 +25,9 @@ go get github.com/sandbox-kit/kit/sdks/go/providers/modal@latest -## Create a sandbox +## Create -Set `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET`, and `MODAL_APP_NAME`. This program uses the environment `main` when `MODAL_ENVIRONMENT` is unset. +Set `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET`, and `MODAL_APP_NAME`. Default environment: `main`. @@ -60,7 +60,7 @@ const sandbox = await client.create({ console.log(sandbox.id); ``` -`timeout` and `maximumLifetime` are milliseconds. `memoryMib` is a bigint. `await using` closes the client when the block ends. Call `await client.close()` in a `finally` block when that syntax is unavailable. +`timeout` and `maximumLifetime` are milliseconds. `memoryMib` is a bigint. @@ -119,14 +119,37 @@ func main() { -Omit `auth` when the Modal SDK should use credentials it already finds in the environment or a Modal profile. `scope.appName` stays required. `endpoint` is rejected. Closing the client does not terminate the sandbox. +Omit `auth` to use Modal SDK / profile credentials. `scope.appName` remains required. `endpoint` is rejected. Closing the client does not terminate the sandbox. -## Where to go next +## Configuration -| You want to | Open | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A command, a region, ports, or an idle timeout | [Create a sandbox](/docs/create#modal) | -| Attach to a sandbox and terminate it | [Manage a sandbox](/docs/lifecycle#terminate) | -| Work with files | [Use files](/docs/filesystem) | -| A failed call | [Read an error](/docs/errors) | -| A project you can run | [create-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-sandbox) and the [Go project](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/configuration/create-modal-sandbox) | +| Setting | Notes | +| ------------- | -------------------------------------------------- | +| Token pair | `auth.tokenPair` / `TokenPair` | +| OAuth refresh | Supported with client id and one secret or JWT key | +| App | `scope.appName` — required | +| Environment | `scope.environment` (often `main`) | +| Region | `region` — default placement preference | +| Endpoint | Unsupported | + +Full rules: [Configuration](/docs/configuration). + +## Operations + +| Area | Support | +| --------- | ----------------------------------------------------------------------------------------- | +| Create | Registry image; runtime, isolation, resources, network, ports, secrets, readiness, labels | +| Open | `attach` (id only). `get` / `refresh` unsupported | +| Lifecycle | `terminate`, `close`. Start/stop/pause/archive/delete unsupported | +| Files | Full filesystem API — [Filesystem](/docs/filesystem) | +| Process | `run` — [Run a command](/docs/process) | + +When set, resources require ≥ 0.125 CPU and ≥ 128 MiB. Disk cannot be raised at create. + +## Related + +| Topic | Link | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Resources, idle, ports | [Create](/docs/create#modal) | +| attach / terminate | [Lifecycle](/docs/lifecycle) | +| Example project | [create-modal-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/configuration/create-modal-sandbox) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/configuration/create-modal-sandbox) | diff --git a/website/content/docs/process.mdx b/website/content/docs/process.mdx new file mode 100644 index 00000000..bf8a26f2 --- /dev/null +++ b/website/content/docs/process.mdx @@ -0,0 +1,341 @@ +--- +title: Run a command +description: Blocking Process.run — argv or shell, cwd, env, timeouts, and exit codes. +--- + +Use `sandbox.process.run` (TypeScript) or `Process().Run` (Go) on an open handle. Providers share the same options and result shape; switch Daytona or Modal above for mapping and edge cases. + +| Method | Returns | +| ------------- | ---------------------------------------------------------------------- | +| `run` / `Run` | `exitCode` / `exit_code`, `stdout`, `stderr` when the command finishes | + +Prerequisite: create or open a sandbox ([Daytona](/docs/daytona), [Modal](/docs/modal), or [Lifecycle](/docs/lifecycle)). + +## Options + +Set **exactly one** of: + +| Field | Meaning | Example | +| ------- | ------------------------ | ------------------- | +| `argv` | Program + arguments | `["echo", "hello"]` | +| `shell` | One shell command string | `"echo hello"` | + +| Field | Rules | +| ------------------ | ------------------------------------------------------------------------------------- | +| Both or neither | Rejected. Field: `argv` | +| Blank `shell` | Rejected after trim (whitespace-only too). Field: `shell` | +| `workingDirectory` | Absolute when set (`/workspace`). Relative, blank, or NUL → field `working_directory` | +| `environment` | For this call only. Omit → provider default. Empty map → add no variables | +| `timeout` | TypeScript: ms. Go: `time.Duration`. See [Timeouts and cancel](#timeouts-and-cancel) | + +### How providers run it + + + +| You pass | On Daytona | +| ---------------------- | --------------------------------------------------------------- | +| `argv` | POSIX-quoted shell string | +| `shell` | shell string as-is | +| omit / zero timeout | no command limit (`0`, so Daytona does not use its 10s default) | +| fractional (`1500` ms) | rejected (`timeout`) | + + + + + +| You pass | On Modal | +| ---------------------- | --------------------------------------------------------------------- | +| `argv` | argument list as-is | +| `shell` | `bash -c` | +| omit / zero timeout | no command limit (TypeScript omits `timeoutMs`; Go uses `Timeout: 0`) | +| fractional (`1500` ms) | rejected (`timeout`) | + + + +### Result + +| Field | Notes | +| ------------------------ | -------------------------------- | +| `exitCode` / `exit_code` | Non-zero is **not** an SDK error | +| `stdout` | Bytes | +| `stderr` | See provider note below | + + + Daytona returns stdout only; `stderr` is empty (Go: non-nil empty slice). + stdout may include merged streams. + + +Modal returns both `stdout` and `stderr`. + +Calls on one handle run one at a time with lifecycle and filesystem. A second +handle for the same sandbox does not wait. + +## Timeouts and cancel + +| Setting | Meaning | +| -------------- | ----------------------------------------------------------------- | +| Omit `timeout` | Use the client default deadline | +| `0` | No extra client deadline for this call | +| Positive value | Cap this call; must be whole seconds (`5000` ok, `1500` rejected) | + +Omit or zero also means no separate provider command limit (see the mapping +table above). + + + Cancel the Go context to end the wait. In TypeScript, abort rejects the + `await`; the remote command may keep running. Either way the handle stays busy + until that provider call settles. + + + + Cancel the Go context to end the wait. In TypeScript, abort rejects the + `await`; the remote command may keep running. Either way the handle stays busy + until that provider call settles. + + +## argv + + + + +```ts +const result = await sandbox.process.run({ + argv: ["echo", "hello from kit"], + timeout: 5_000, +}); +console.log(result.exitCode, new TextDecoder().decode(result.stdout)); +``` + + + + +```go +import ( + "fmt" + "time" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +result, err := instance.Process().Run(ctx, &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, + Timeout: sandbox.Duration(5 * time.Second), +}) +if err != nil { + return err +} +fmt.Printf("exit=%d stdout=%q\n", result.GetExitCode(), string(result.GetStdout())) +``` + + + + + + Examples: + [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process/run-daytona-command) + · + [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process/run-daytona-command) + + + + Examples: + [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process/run-modal-command) + · + [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process/run-modal-command) + + +## shell + +Do not set `argv` on the same call. + + + + +```ts +const result = await sandbox.process.run({ + shell: 'echo "hello from shell"', +}); +``` + + + Daytona runs the string as shell. On Modal the same call uses `bash -c`. + + + + Modal runs this as `bash -c`. On Daytona the same call uses the shell string + as-is. + + + + + +```go +import "github.com/sandbox-kit/kit/sdks/go/sandbox" + +result, err := instance.Process().Run(ctx, &sandbox.ExecOptions{ + Shell: sandbox.Value(`echo "hello from shell"`), +}) +``` + + + + + + Examples: + [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process/run-daytona-shell-command) + · + [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process/run-daytona-shell-command) + + + + Examples: + [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process/run-modal-shell-command) + · + [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process/run-modal-shell-command) + + +## Working directory and environment + +Applies to that call only. + + + + +```ts +const result = await sandbox.process.run({ + argv: ["printenv", "KIT_DEMO"], + workingDirectory: "/tmp", + environment: { KIT_DEMO: "from-kit" }, +}); +``` + + + + +```go +import "github.com/sandbox-kit/kit/sdks/go/sandbox" + +result, err := instance.Process().Run(ctx, &sandbox.ExecOptions{ + Argv: []string{"printenv", "KIT_DEMO"}, + WorkingDirectory: sandbox.Value("/tmp"), + Environment: map[string]string{"KIT_DEMO": "from-kit"}, +}) +``` + + + + + +cwd: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process/run-daytona-with-cwd) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process/run-daytona-with-cwd) + +env: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process/run-daytona-with-env) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process/run-daytona-with-env) + + + + +cwd: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process/run-modal-with-cwd) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process/run-modal-with-cwd) + +env: [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process/run-modal-with-env) · [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process/run-modal-with-env) + + + +## Timeout + +Cap one call with a whole-second `timeout`. Fractional values are rejected (`timeout`). + + + + +```ts +const result = await sandbox.process.run({ + argv: ["echo", "hello with timeout"], + timeout: 5_000, +}); +``` + + + + +```go +result, err := instance.Process().Run(ctx, &sandbox.ExecOptions{ + Argv: []string{"echo", "hello with timeout"}, + Timeout: sandbox.Duration(5 * time.Second), +}) +``` + + + + + + Examples: + [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process/run-daytona-with-timeout) + · + [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process/run-daytona-with-timeout) + + + + Examples: + [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process/run-modal-with-timeout) + · + [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process/run-modal-with-timeout) + + +## Non-zero exit code + +Transport and validation failures still return errors. A finished command with exit `7` does not. + + + + +```ts +const result = await sandbox.process.run({ shell: "exit 7" }); +if (result.exitCode !== 7) { + throw new Error(`want exit 7, got ${result.exitCode}`); +} +``` + + + + +```go +import ( + "fmt" + + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +result, err := instance.Process().Run(ctx, &sandbox.ExecOptions{ + Shell: sandbox.Value("exit 7"), +}) +if err != nil { + return err +} +if result.GetExitCode() != 7 { + return fmt.Errorf("want exit 7, got %d", result.GetExitCode()) +} +``` + + + + + + Examples: + [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/process/run-daytona-check-exit) + · + [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/daytona/process/run-daytona-check-exit) + + + + Examples: + [TypeScript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/modal/process/run-modal-check-exit) + · + [Go](https://github.com/sandbox-kit/kit/tree/main/examples/go/modal/process/run-modal-check-exit) + + +## Limits + +Not supported: streaming I/O, interactive sessions, PTY. + + + Daytona `code_run` is not available on this API. + diff --git a/website/src/components/home-code-sample.tsx b/website/src/components/home-code-sample.tsx new file mode 100644 index 00000000..c55ee990 --- /dev/null +++ b/website/src/components/home-code-sample.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { useState, type ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +const samples = { + TypeScript: { + filename: "create.ts", + code: `import { daytona } from "@sandbox-kit/daytona"; +import { Client } from "@sandbox-kit/sandbox"; + +await using client = new Client({ + provider: daytona(), + auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } }, +}); + +const sandbox = await client.create(); +const result = await sandbox.process.run({ + argv: ["echo", "hello from kit"], +}); + +console.log(result.exitCode, result.stdout);`, + }, + Go: { + filename: "create.go", + code: `import ( + "context" + "os" + + "github.com/sandbox-kit/kit/sdks/go/providers/daytona" + "github.com/sandbox-kit/kit/sdks/go/sandbox" +) + +client, err := sandbox.NewClient(daytona.New(), &sandbox.Config{ + Auth: &sandbox.AuthConfig{Credentials: &sandbox.APIKey{ + ApiKey: &sandbox.APIKeyCredentials{ + Key: os.Getenv("DAYTONA_API_KEY"), + }, + }}, +}) +defer client.Close(context.Background()) + +instance, err := client.Create(context.Background(), nil) +result, err := instance.Process().Run( + context.Background(), + &sandbox.ExecOptions{ + Argv: []string{"echo", "hello from kit"}, + }, +)`, + }, +} as const; + +type Lang = keyof typeof samples; + +function highlight(code: string, lang: Lang) { + const keyword = + lang === "TypeScript" + ? /\b(import|from|await|using|const|new|console)\b/g + : /\b(import|defer|err|nil|NewClient|New|Create|Run|Background|Getenv)\b/g; + + const stringRe = /("(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`)/g; + const commentRe = /(\/\/.*$)/gm; + + const parts: { text: string; kind: "plain" | "string" | "comment" }[] = []; + let cursor = 0; + const marks: { start: number; end: number; kind: "string" | "comment" }[] = + []; + + for (const match of code.matchAll(commentRe)) { + if (match.index == null) continue; + marks.push({ + start: match.index, + end: match.index + match[0].length, + kind: "comment", + }); + } + for (const match of code.matchAll(stringRe)) { + if (match.index == null) continue; + const overlaps = marks.some( + (m) => match.index! < m.end && match.index! + match[0].length > m.start, + ); + if (!overlaps) { + marks.push({ + start: match.index, + end: match.index + match[0].length, + kind: "string", + }); + } + } + marks.sort((a, b) => a.start - b.start); + + for (const mark of marks) { + if (mark.start > cursor) { + parts.push({ text: code.slice(cursor, mark.start), kind: "plain" }); + } + parts.push({ text: code.slice(mark.start, mark.end), kind: mark.kind }); + cursor = mark.end; + } + if (cursor < code.length) + parts.push({ text: code.slice(cursor), kind: "plain" }); + + return parts.map((part, i) => { + if (part.kind === "string") { + return ( + + {part.text} + + ); + } + if (part.kind === "comment") { + return ( + + {part.text} + + ); + } + const nodes: ReactNode[] = []; + let last = 0; + for (const match of part.text.matchAll(keyword)) { + if (match.index == null) continue; + if (match.index > last) nodes.push(part.text.slice(last, match.index)); + nodes.push( + + {match[0]} + , + ); + last = match.index + match[0].length; + } + if (last < part.text.length) nodes.push(part.text.slice(last)); + return {nodes}; + }); +} + +export function HomeCodeSample() { + const [lang, setLang] = useState("TypeScript"); + const sample = samples[lang]; + const lines = sample.code.split("\n"); + + return ( +
+
+ {(Object.keys(samples) as Lang[]).map((item) => ( + + ))} + + {sample.filename} + +
+
+        
+          {lines.map((line, index) => (
+            
+ + {index + 1} + + + {highlight(line || " ", lang)} + +
+ ))} +
+
+
+ ); +} diff --git a/website/src/components/mdx.tsx b/website/src/components/mdx.tsx index eec05ff0..ea0c5b3f 100644 --- a/website/src/components/mdx.tsx +++ b/website/src/components/mdx.tsx @@ -1,12 +1,14 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; -import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import type { MDXComponents } from "mdx/types"; +import { ForProvider } from "@/components/provider-context"; +import { Tab, Tabs } from "@/components/tabs"; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, Tabs, Tab, + ForProvider, ...components, } satisfies MDXComponents; } diff --git a/website/src/components/provider-context.tsx b/website/src/components/provider-context.tsx new file mode 100644 index 00000000..b3d36068 --- /dev/null +++ b/website/src/components/provider-context.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { + createContext, + useCallback, + useContext, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import { Check, ChevronDown } from "lucide-react"; +import { buttonVariants } from "fumadocs-ui/components/ui/button"; +import { cn } from "@/lib/cn"; +import { + DEFAULT_DOC_PROVIDER, + DOC_PROVIDER_STORAGE_KEY, + DOC_PROVIDERS, + isDocProviderId, + type DocProviderId, +} from "@/lib/providers"; + +type ProviderContextValue = { + provider: DocProviderId; + setProvider: (id: DocProviderId) => void; + providers: typeof DOC_PROVIDERS; +}; + +const ProviderContext = createContext(null); + +export function ProviderProvider({ children }: { children: ReactNode }) { + const [provider, setProviderState] = + useState(DEFAULT_DOC_PROVIDER); + + useLayoutEffect(() => { + try { + const stored = localStorage.getItem(DOC_PROVIDER_STORAGE_KEY); + if (stored && isDocProviderId(stored)) { + setProviderState(stored); + } + } catch { + // ignore + } + }, []); + + const setProvider = useCallback((id: DocProviderId) => { + setProviderState(id); + try { + localStorage.setItem(DOC_PROVIDER_STORAGE_KEY, id); + } catch { + // ignore + } + }, []); + + const value = useMemo( + () => ({ + provider, + setProvider, + providers: DOC_PROVIDERS, + }), + [provider, setProvider], + ); + + return ( + + {children} + + ); +} + +export function useDocProvider(): ProviderContextValue { + const ctx = useContext(ProviderContext); + if (!ctx) { + throw new Error("useDocProvider must be used within ProviderProvider"); + } + return ctx; +} + +export function ProviderSelect({ className }: { className?: string }) { + const { provider, setProvider, providers } = useDocProvider(); + const [open, setOpen] = useState(false); + const rootRef = useRef(null); + const current = + providers.find((item) => item.id === provider) ?? providers[0]!; + + useEffect(() => { + if (!open) return; + const onPointerDown = (event: PointerEvent) => { + if (!rootRef.current?.contains(event.target as Node)) setOpen(false); + }; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setOpen(false); + }; + document.addEventListener("pointerdown", onPointerDown); + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("pointerdown", onPointerDown); + document.removeEventListener("keydown", onKeyDown); + }; + }, [open]); + + return ( +
+ + {open && ( +
+ {providers.map((item) => { + const selected = item.id === provider; + return ( + + ); + })} +
+ )} +
+ ); +} + +/** Pages that are already scoped to one provider — no page-level selector. */ +const PROVIDER_SCOPED_PAGES = new Set(["daytona", "modal"]); + +export function DocsProviderSelect({ path }: { path: string }) { + const slug = + path + .replace(/\.mdx?$/, "") + .split("/") + .pop() ?? ""; + if (PROVIDER_SCOPED_PAGES.has(slug)) return null; + return ; +} + +export function ForProvider({ + id, + children, +}: { + id: DocProviderId | DocProviderId[]; + children: ReactNode; +}) { + const { provider } = useDocProvider(); + const ids = Array.isArray(id) ? id : [id]; + if (!ids.includes(provider)) return null; + return children; +} diff --git a/website/src/components/tabs.tsx b/website/src/components/tabs.tsx new file mode 100644 index 00000000..ee7f258c --- /dev/null +++ b/website/src/components/tabs.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { + createContext, + useContext, + useEffect, + useId, + useMemo, + type ComponentProps, + type ReactNode, +} from "react"; +import { + Tabs as RootTabs, + TabsContent as RootTabsContent, + TabsList as RootTabsList, + TabsTrigger as RootTabsTrigger, +} from "fumadocs-ui/components/ui/tabs"; +import { cn } from "@/lib/cn"; + +type TabsContextValue = { + items?: string[]; + collection: string[]; +}; + +const TabsContext = createContext(null); + +function useTabContext() { + const ctx = useContext(TabsContext); + if (!ctx) throw new Error("You must wrap your component in "); + return ctx; +} + +function escapeValue(v: string) { + return v.toLowerCase().replace(/\s/, "-"); +} + +type RootTabsProps = ComponentProps; + +export type TabsProps = Omit & { + items?: string[]; + defaultIndex?: number; + label?: ReactNode; +}; + +export function Tabs({ + ref, + className, + items, + label, + defaultIndex = 0, + defaultValue = items ? escapeValue(items[defaultIndex]!) : undefined, + children, + ...props +}: TabsProps) { + const collection = useMemo(() => [] as string[], []); + + return ( + + {items && ( + + {label && ( + {label} + )} + {items.map((item) => ( + + {item} + + ))} + + )} + ({ + items, + collection, + }), + [collection, items], + )} + > + {children} + + + ); +} + +export type TabProps = Omit, "value"> & { + value?: string; +}; + +export function Tab({ value, ...props }: TabProps) { + const { items } = useTabContext(); + const resolved = value ?? items?.at(useCollectionIndex()); + if (!resolved) { + throw new Error( + "Failed to resolve tab `value`, please pass a `value` prop to the Tab component.", + ); + } + return ( + + ); +} + +function useCollectionIndex() { + const key = useId(); + const { collection } = useTabContext(); + useEffect(() => { + return () => { + const idx = collection.indexOf(key); + if (idx !== -1) collection.splice(idx, 1); + }; + }, [key, collection]); + if (!collection.includes(key)) collection.push(key); + return collection.indexOf(key); +} diff --git a/website/src/lib/providers.ts b/website/src/lib/providers.ts new file mode 100644 index 00000000..01ade209 --- /dev/null +++ b/website/src/lib/providers.ts @@ -0,0 +1,14 @@ +export const DOC_PROVIDERS = [ + { id: "daytona", label: "Daytona" }, + { id: "modal", label: "Modal" }, +] as const; + +export type DocProviderId = (typeof DOC_PROVIDERS)[number]["id"]; + +export const DEFAULT_DOC_PROVIDER: DocProviderId = "daytona"; + +export const DOC_PROVIDER_STORAGE_KEY = "sandbox-kit.docs.provider"; + +export function isDocProviderId(value: string): value is DocProviderId { + return DOC_PROVIDERS.some((provider) => provider.id === value); +} diff --git a/website/src/lib/shared.ts b/website/src/lib/shared.ts index f4c4ac23..d6f8e996 100644 --- a/website/src/lib/shared.ts +++ b/website/src/lib/shared.ts @@ -39,6 +39,24 @@ export function assetUrl(path: string) { return `${base}${path.replace(/^\//, "")}`; } +/** Absolute URL for a root-relative site path (applies `BASE_URL` once). */ +export function absoluteSiteUrl(path: string) { + const base = (import.meta.env.BASE_URL ?? "/").replace(/\/$/, ""); + const normalized = path.startsWith("/") ? path : `/${path}`; + const withBase = + !base || normalized === base || normalized.startsWith(`${base}/`) + ? normalized + : `${base}${normalized}`; + if (typeof window !== "undefined") { + return `${window.location.origin}${withBase}`; + } + // Static prerender for GitHub Pages project site. + if (base === "/kit") { + return `https://sandbox-kit.github.io${withBase}`; + } + return withBase; +} + const getImageUrl = createGetUrl(docsImageRoute); export function getPageImageUrl(page: { slugs: string[]; locale?: string }) { diff --git a/website/src/routes/__root.tsx b/website/src/routes/__root.tsx index 0f9dd15e..b8f3b4e9 100644 --- a/website/src/routes/__root.tsx +++ b/website/src/routes/__root.tsx @@ -7,6 +7,7 @@ import { import * as React from "react"; import appCss from "@/styles/app.css?url"; import { RootProvider } from "fumadocs-ui/provider/tanstack"; +import { ProviderProvider } from "@/components/provider-context"; import { assetUrl } from "@/lib/shared"; export const Route = createRootRoute({ @@ -21,7 +22,7 @@ export const Route = createRootRoute({ { name: "description", content: - "One interface for your sandbox providers. The client an agent uses to work in a cloud sandbox, from TypeScript or Go.", + "Lightweight TypeScript and Go SDK for cloud sandboxes. Pluggable providers, one client for create, lifecycle, files, and commands.", }, ], links: [ @@ -43,7 +44,9 @@ function RootComponent() { theme={{ defaultTheme: "light" }} search={{ options: { type: "static" } }} > - + + + diff --git a/website/src/routes/docs/$.tsx b/website/src/routes/docs/$.tsx index bc36f097..dd025520 100644 --- a/website/src/routes/docs/$.tsx +++ b/website/src/routes/docs/$.tsx @@ -11,13 +11,14 @@ import { } from "fumadocs-ui/layouts/docs/page"; import { baseOptions } from "@/lib/layout.shared"; import { + absoluteSiteUrl, assetUrl, getPageImageUrl, getPageMarkdownUrl, gitConfig, } from "@/lib/shared"; -import { useFumadocsLoader } from "fumadocs-core/source/client"; -import { Suspense, use } from "react"; +import { Suspense, use, useEffect, useState } from "react"; +import { DocsProviderSelect } from "@/components/provider-context"; import { useMDXComponents } from "@/components/mdx"; export const Route = createFileRoute("/docs/$")({ @@ -32,9 +33,9 @@ export const Route = createFileRoute("/docs/$")({ path: page.path, title: page.data.title, description: page.data.description, - markdownUrl: assetUrl(getPageMarkdownUrl(page).url), + // Root-relative without BASE_URL — fumadocs page actions add BASE_URL once. + markdownUrl: getPageMarkdownUrl(page).url, imageUrl: assetUrl(getPageImageUrl(page).url), - pageTree: await source.serializePageTree(source.getPageTree()), }; }, head: ({ loaderData }) => ({ @@ -54,6 +55,11 @@ function Content({ path, markdownUrl }: { path: string; markdownUrl: string }) { const { toc } = use(page.load()); const MDX = page.body; + // AI tools fetch this URL; point at the static .md file, not the SPA HTML page. + const [markdownPageUrl, setMarkdownPageUrl] = useState(markdownUrl); + useEffect(() => { + setMarkdownPageUrl(absoluteSiteUrl(markdownUrl)); + }, [markdownUrl]); return ( @@ -63,8 +69,10 @@ function Content({ path, markdownUrl }: { path: string; markdownUrl: string }) { + @@ -74,9 +82,11 @@ function Content({ path, markdownUrl }: { path: string; markdownUrl: string }) { } function Page() { - const { path, pageTree, markdownUrl } = useFumadocsLoader( - Route.useLoaderData(), - ); + const { path, markdownUrl } = Route.useLoaderData(); + // Build the tree in the component (SSR + client). Do not put it through + // serializePageTree: that imports react-dom/server.edge (CJS `require`), which + // breaks SPA navigation and has no server to call on GitHub Pages. + const pageTree = source.getPageTree(); return ( diff --git a/website/src/routes/index.tsx b/website/src/routes/index.tsx index b2e27822..4dea1676 100644 --- a/website/src/routes/index.tsx +++ b/website/src/routes/index.tsx @@ -1,5 +1,6 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { HomeLayout } from "fumadocs-ui/layouts/home"; +import { HomeCodeSample } from "@/components/home-code-sample"; import { baseOptions } from "@/lib/layout.shared"; import { assetUrl } from "@/lib/shared"; @@ -9,35 +10,71 @@ export const Route = createFileRoute("/")({ function Home() { return ( - -
- +
+
-

- One interface for your sandbox providers. -

-

- Sandbox Kit is the client an agent uses with a cloud sandbox. Pass in - a provider. The same client opens the sandbox where that agent writes - files, clones a repository, and runs its work. -

-

- - Read the guide - - . Open the page for the call you need. TypeScript and Go samples stay - on the language you pick. -

-
+
+ +
+
+ +

+ Sandbox Kit +

+

+ Lightweight TypeScript and Go SDK for cloud sandboxes. Plug in a + provider—one client for create, files, and commands. +

+
+ + Quickstart + + + Concepts + +
+

+ Daytona · Modal · your provider next +

+
+ +
+

+ Same call shape on every provider +

+
+ +
+
+
+
); } diff --git a/website/src/styles/app.css b/website/src/styles/app.css index 401b0791..1dd2801b 100644 --- a/website/src/styles/app.css +++ b/website/src/styles/app.css @@ -65,3 +65,42 @@ html > body[data-scroll-locked] { body { font-family: var(--font-sans); } + +/* Landing: one viewport, brand left + code as the visual plane. */ +.home-shell { + height: 100dvh; + max-height: 100dvh; +} + +.home-copy, +.home-sample, +.home-code { + animation: home-rise 700ms cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.home-sample { + animation-delay: 90ms; +} + +.home-code { + animation-delay: 160ms; +} + +@keyframes home-rise { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .home-copy, + .home-sample, + .home-code { + animation: none; + } +} diff --git a/website/vite.config.ts b/website/vite.config.ts index fd430c24..29de8115 100644 --- a/website/vite.config.ts +++ b/website/vite.config.ts @@ -1,16 +1,18 @@ -import react from '@vitejs/plugin-react'; -import { tanstackStart } from '@tanstack/react-start/plugin/vite'; -import { defineConfig } from 'vite'; -import tailwindcss from '@tailwindcss/vite'; -import { fumadocsMdx } from 'fumadocs-mdx/vite'; -import { nitro } from 'nitro/vite'; +import react from "@vitejs/plugin-react"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import { defineConfig } from "vite"; +import tailwindcss from "@tailwindcss/vite"; +import { fumadocsMdx } from "fumadocs-mdx/vite"; +import { nitro } from "nitro/vite"; // GitHub project Pages serves this repo at /kit/. Local dev leaves DOCS_BASE unset. -const base = process.env.DOCS_BASE ?? '/'; -const basePath = base === '/' ? '' : base.replace(/\/$/, ''); +const base = process.env.DOCS_BASE ?? "/"; +const basePath = base === "/" ? "" : base.replace(/\/$/, ""); export default defineConfig({ base, + // Prefer a project-local cache so root-owned node_modules/.vite does not block local dev. + cacheDir: process.env.VITE_CACHE_DIR ?? ".vite", server: { port: 3000, }, @@ -26,13 +28,13 @@ export default defineConfig({ }), react(), nitro({ - preset: process.env.NITRO_PRESET ?? 'vercel', + preset: process.env.NITRO_PRESET ?? "vercel", }), ], resolve: { tsconfigPaths: true, alias: { - tslib: 'tslib/tslib.es6.js', + tslib: "tslib/tslib.es6.js", }, }, });