Repository navigation
Add shared Process.Run for Daytona and Modal - #40
Conversation
Expose one blocking command surface on the sandbox handle so callers get exit code, stdout, and stderr without provider-specific exec APIs. Reject argv/shell and path mistakes in Kit, map whole-second timeouts honestly, and keep sessions, PTY, and streaming out of this cut. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6G6CECRB6HJBQW71VA48Q5S
Expand shell/cwd/env/exit coverage across providers and keep npm, pkg.go.dev, and website docs aligned with the shared process surface. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6G76W4MQK433BRHYDBMDZPC
Guide samples and edge cases follow the selected provider, and the landing page shows a TypeScript/Go create-and-run snippet without scrolling. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6GA167VFNR88C6H0YC83GD8
Avoid double-prefixing markdown URLs under /kit, point AI open links at the static .md files, and style the provider menu like the other page actions. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6GAMHJX21Q5MJYBW5WSNNPM
Omit Modal TypeScript timeoutMs when unset or zero so native exec no longer rejects the default path, keep Daytona stderr non-nil empty, and align validation inventory, tests, and user-facing process docs with the wire behavior. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6H0V2KEEVBD4ZW1N7C7KFKS
Preserve empty env maps and empty stdout/stderr through Go Process.Run, share whole-second timeout mapping, and document Daytona/Modal timeout, empty-env, and TypeScript soft-abort behavior honestly. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6JM18BHS64VZ626305YECMT
Close exec stdout/stderr after a failed Wait so ReadAll does not hang on Background-wired Modal streams after the Kit deadline. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6JMP3MEVCQZY68M33PP2S6W
Clarify timeouts, cancel, env, and provider output in guides, npm/pkg.go.dev READMEs, and process example edge cases without kit-builder jargon. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6JN1QZXJ7MR7WGZJHWBX5WF
Build the sidebar page tree in the component instead of serializePageTree, which pulls in react-dom/server.edge and breaks client-side route changes with no server to call on github.io. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6JNSSH6Z8AKV63P026ND35D
…ettle. Abort and timeout still reject the caller promptly, but the next lifecycle, filesystem, or process call on the same handle waits until the native work finishes—matching Go and the documented one-at-a-time contract. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6JQBRJZNMFPVHRK29TFY1CY
Code blocks that use Client, providers, LifecycleCapability, or Go sandbox types now show the imports needed to copy them into a project. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6JR1CT5HHXA8TTCX1M4KYSF
Document shell blankness after trim in the validation inventory so the timeout and cancel story matches what callers and tests already enforce. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M6JSRGZB4B1SSGVCP2WFVTF0
|
someone cut corners## Summary Adds a shared blocking command API on the sandbox handle, documents it across examples and the docs site, and hardens Go cloning / timeout / Modal cancel behavior against real Daytona and Modal SDK behavior.
Non-zero exit codes are successful results, not SDK errors. Sessions, PTY, streaming handles, and Daytona What changed (by layer)flowchart TB
subgraph contracts [Contracts]
Proto["proto ExecOptions / ExecResult"]
Inventory["specs/validation.yaml ExecOptions"]
end
subgraph core [Core SDKs]
GoCore["Go sandbox.Process"]
TSCore["TS sandbox.process"]
Clone["Go cloneExecOptions / cloneExecResult"]
end
subgraph providers [Providers]
DGo["Go Daytona"]
DTS["TS Daytona"]
MGo["Go Modal + stream close on cancel"]
MTS["TS Modal"]
end
subgraph docs [Docs and site]
Guide["docs/process.md + /docs/process"]
Examples["24 process examples"]
Site["Provider switch + home sample"]
end
Proto --> GoCore
Proto --> TSCore
Inventory --> GoCore
Inventory --> TSCore
GoCore --> Clone
GoCore --> DGo
GoCore --> MGo
TSCore --> DTS
TSCore --> MTS
GoCore --> Guide
TSCore --> Guide
Guide --> Examples
Guide --> Site
Call pathsequenceDiagram
participant App
participant Kit as Kit Process.Run
participant Validate as Kit validation
participant Adapter as Daytona or Modal adapter
participant Native as Provider SDK
App->>Kit: Run(argv|shell, cwd?, env?, timeout?)
Kit->>Validate: argv XOR shell, blank shell after trim, absolute cwd
alt invalid
Validate-->>App: InvalidArgument (field set)
else valid
Kit->>Kit: operation deadline from timeout / client
Kit->>Adapter: mapped request
Adapter->>Adapter: whole-second timeout gate
Adapter->>Native: executeCommand / sandbox.exec
Native-->>Adapter: exit + streams
Adapter-->>Kit: ExecResult
Note over Kit: Go preserves empty env / empty stdout|stderr after clone
Kit-->>App: ExecResult (nonzero exit is OK)
end
Provider mappingflowchart LR
subgraph kit [Kit ExecOptions]
Argv["argv"]
Shell["shell"]
Cwd["working_directory"]
Env["environment"]
Timeout["timeout"]
end
subgraph daytona [Daytona]
DCmd["POSIX-quoted shell string"]
DShell["shell as-is"]
DExec["executeCommand"]
DOut["stdout only; stderr empty"]
DT0["timeout 0 when Kit omit/zero<br/>avoids native 10s default"]
end
subgraph modal [Modal]
MArgv["argv as-is"]
MBash["bash -c shell"]
MExec["sandbox.exec + drain pipes"]
MOut["stdout + stderr"]
MT0["Go Timeout 0 / TS omit timeoutMs<br/>never send timeoutMs: 0"]
end
Argv --> DCmd --> DExec --> DOut
Shell --> DShell --> DExec
Timeout --> DT0 --> DExec
Argv --> MArgv --> MExec --> MOut
Shell --> MBash --> MExec
Timeout --> MT0 --> MExec
Cwd --> DExec
Cwd --> MExec
Env --> DExec
Env --> MExec
Review fixes in this branch1. Go empty-collection cloning (bug fix)
Fix: flowchart LR
In["ExecOptions / ExecResult<br/>empty env or []byte{}"] --> Clone["cloneExec*"]
Clone --> Out["owned copy<br/>empties preserved"]
Out --> Provider["provider sees non-nil empty env"]
Out --> Caller["caller sees non-nil empty stderr"]
2. Go Modal cancel no longer hangs on stdio drain (bug fix)Modal wires exec stdout/stderr with Fix: on sequenceDiagram
participant Kit as collectProcess
participant Wait as process.Wait(ctx)
participant Out as stdout/stderr ReadAll
Kit->>Out: start drain goroutines
Kit->>Wait: wait for exit
Note over Wait: ctx canceled / deadline
Wait-->>Kit: error
Kit->>Out: Close streams
Out-->>Kit: reads unblock
Kit-->>Kit: return wait error
3. Shared whole-second timeout helper
4. Docs honesty
5. Daytona TypeScript process testsCovers argv POSIX quoting, omit/zero → 6. Small cleanups
7. TypeScript handle serialization holds through in-flight abort (bug fix)Abort/timeout must reject the caller promptly, but the next lifecycle, filesystem, or process call on the same handle must not overlap native work that providers ignore mid-call. Matching Go and the documented one-at-a-time contract:
Lifecycle / process / filesystem paths stop wrapping provider calls in a second factory-style sequenceDiagram
participant App
participant Gate as Sandbox.#serialized
participant Native as Provider call
App->>Gate: start({ signal })
Gate->>Native: work = body(signal)
Note over App: abort / deadline
Gate-->>App: reject Canceled (prompt)
Note over Gate,Native: gate still held
App->>Gate: start() / run() / readFile()
Note over Gate: waits on previous unlock
Native-->>Gate: settles
Gate->>Gate: unlock
Gate->>Native: second provider call
Native-->>App: result
flowchart TB
subgraph before [Before: unlock on caller reject]
B1["acquire gate"] --> B2["awaitWithSignal factory"]
B2 -->|"abort rejects"| B3["finally unlock"]
B3 --> B4["next call starts<br/>while native still in flight"]
end
subgraph after [After: unlock when work settles]
A1["acquire gate"] --> A2["work = body(signal)"]
A2 --> A3["awaitWithSignal(work)"]
A3 -->|"abort rejects caller"| A4["finally: unlock on work.then"]
A4 --> A5["next call waits<br/>until native settles"]
end
Covered by client tests: aborted in-flight 8. Go process cancel + serialization parity (follow-up)Go now mirrors the TypeScript busy-handle story for Process: sequenceDiagram
participant App
participant Gate as operationGate
participant Run as Process.Run
participant Native as provider RunCommand
App->>Run: Run(ctx1, sleep)
Run->>Gate: acquire
Run->>Native: start (blocks)
App->>App: cancel ctx1
Note over Native: still held until release
App->>Run: Run(ctx2, true)
Note over Gate: second call waits
Native-->>Run: ctx canceled
Run->>Gate: release
Run-->>App: ErrorKindCanceled
Gate->>Native: second RunCommand
Native-->>App: result
Docs site
Examples24 runnable projects under
flowchart LR
subgraph topics [Process example topics]
A[command]
B[shell]
C[cwd]
D[env]
E[timeout]
F[check-exit]
end
subgraph matrix [x2 providers x2 languages]
G[Go Daytona]
H[TS Daytona]
I[Go Modal]
J[TS Modal]
end
topics --> matrix
Test plan
|
Summary
Adds a shared blocking command API on the sandbox handle, documents it across examples and the docs site, and hardens Go cloning / timeout / Modal cancel behavior against real Daytona and Modal SDK behavior.
instance.Process().Run(ctx, opts)sandbox.process.run(opts)Argv/Shellargv/shellexit_code,stdout,stderrexitCode,stdout,stderrNon-zero exit codes are successful results, not SDK errors. Sessions, PTY, streaming handles, and Daytona
code_runstay out of this cut.What changed (by layer)
flowchart TB subgraph contracts [Contracts] Proto["proto ExecOptions / ExecResult"] Inventory["specs/validation.yaml ExecOptions"] end subgraph core [Core SDKs] GoCore["Go sandbox.Process"] TSCore["TS sandbox.process"] Clone["Go cloneExecOptions / cloneExecResult"] end subgraph providers [Providers] DGo["Go Daytona"] DTS["TS Daytona"] MGo["Go Modal + stream close on cancel"] MTS["TS Modal"] end subgraph docs [Docs and site] Guide["docs/process.md + /docs/process"] Examples["20 process examples"] Site["Provider switch + home sample"] end Proto --> GoCore Proto --> TSCore Inventory --> GoCore Inventory --> TSCore GoCore --> Clone GoCore --> DGo GoCore --> MGo TSCore --> DTS TSCore --> MTS GoCore --> Guide TSCore --> Guide Guide --> Examples Guide --> SiteCall path
sequenceDiagram participant App participant Kit as Kit Process.Run participant Validate as Kit validation participant Adapter as Daytona or Modal adapter participant Native as Provider SDK App->>Kit: Run(argv|shell, cwd?, env?, timeout?) Kit->>Validate: argv XOR shell, blank shell, absolute cwd alt invalid Validate-->>App: InvalidArgument (field set) else valid Kit->>Kit: operation deadline from timeout / client Kit->>Adapter: mapped request Adapter->>Adapter: whole-second timeout gate Adapter->>Native: executeCommand / sandbox.exec Native-->>Adapter: exit + streams Adapter-->>Kit: ExecResult Note over Kit: Go preserves empty env / empty stdout|stderr after clone Kit-->>App: ExecResult (nonzero exit is OK) endProvider mapping
flowchart LR subgraph kit [Kit ExecOptions] Argv["argv"] Shell["shell"] Cwd["working_directory"] Env["environment"] Timeout["timeout"] end subgraph daytona [Daytona] DCmd["POSIX-quoted shell string"] DShell["shell as-is"] DExec["executeCommand"] DOut["stdout only; stderr empty"] DT0["timeout 0 when Kit omit/zero<br/>avoids native 10s default"] end subgraph modal [Modal] MArgv["argv as-is"] MBash["bash -c shell"] MExec["sandbox.exec + drain pipes"] MOut["stdout + stderr"] MT0["Go Timeout 0 / TS omit timeoutMs<br/>never send timeoutMs: 0"] end Argv --> DCmd --> DExec --> DOut Shell --> DShell --> DExec Timeout --> DT0 --> DExec Argv --> MArgv --> MExec --> MOut Shell --> MBash --> MExec Timeout --> MT0 --> MExec Cwd --> DExec Cwd --> MExec Env --> DExec Env --> MExecargv: ["echo", "hi"]shell: "echo hi"["bash", "-c", "echo hi"]0timeoutMs; GoTimeout: 0)1500ms)timeout)timeout)environment{}Review fixes in this branch
1. Go empty-collection cloning (bug fix)
proto.Clonedrops empty maps and empty byte slices. Process used the naivecloneMessage, so:Environment: map[string]string{}becamenilbefore the providerStderr: []byte{}(and empty stdout) becamenilafter returnFix:
cloneExecOptions/cloneExecResult(same pattern as Create). Covered by unit +Process.Runtests.flowchart LR In["ExecOptions / ExecResult<br/>empty env or []byte{}"] --> Clone["cloneExec*"] Clone --> Out["owned copy<br/>empties preserved"] Out --> Provider["provider sees non-nil empty env"] Out --> Caller["caller sees non-nil empty stderr"]2. Go Modal cancel no longer hangs on stdio drain (bug fix)
Modal wires exec stdout/stderr with
context.Background()(and no stream deadline when command timeout is 0). Kit'sWait(ctx)respects cancel, but the oldcollectProcessstillwg.Wait()'d onio.ReadAll, so a client deadline could leave the caller blocked until the remote process EOFs.Fix: on
Waitfailure, close stdout/stderr so blocked reads unblock (matches Modal's Close-unblocks-read contract), then return the wait error.3. Shared whole-second timeout helper
sdk.WholeSecondDurationshared by Go Daytona and Modal adapters. Behavior unchanged: nil/zero →0; non-whole seconds rejected.4. Docs honesty
0(native default is 10s)timeoutMs(Modal rejects0)await; Daytona/Modal exec often ignoreAbortSignalso the command may keep running. The handle still stays busy until that provider call settles (see §7).{}coerce)5. Daytona TypeScript process tests
Covers argv POSIX quoting, omit/zero →
0, cwd/env forwarding, fractional reject, empty stderr.6. Small cleanups
absolutePathfor sandbox path + working directory#file→#withSandbox7. TypeScript handle serialization holds through in-flight abort (bug fix)
Abort/timeout must reject the caller promptly, but the next lifecycle, filesystem, or process call on the same handle must not overlap native work that providers ignore mid-call. Matching Go and the documented one-at-a-time contract:
#serializedstarts the provider promise (work) once the gate is acquired.awaitWithSignal(work, signal)races that already-started promise so abort rejects without dropping it.finallyunlocks whenworksettles (not when the caller rejects), so a queued follow-up waits.Lifecycle / process / filesystem paths stop wrapping provider calls in a second factory-style
awaitWithSignalthat could start work after unlock. Docs (concepts, lifecycle, filesystem, process) state the busy-until-settle rule.sequenceDiagram participant App participant Gate as Sandbox.#serialized participant Native as Provider call App->>Gate: start({ signal }) Gate->>Native: work = body(signal) Note over App: abort / deadline Gate-->>App: reject Canceled (prompt) Note over Gate,Native: gate still held App->>Gate: start() / run() / readFile() Note over Gate: waits on previous unlock Native-->>Gate: settles Gate->>Gate: unlock Gate->>Native: second provider call Native-->>App: resultflowchart TB subgraph before [Before: unlock on caller reject] B1["acquire gate"] --> B2["awaitWithSignal factory"] B2 -->|"abort rejects"| B3["finally unlock"] B3 --> B4["next call starts<br/>while native still in flight"] end subgraph after [After: unlock when work settles] A1["acquire gate"] --> A2["work = body(signal)"] A2 --> A3["awaitWithSignal(work)"] A3 -->|"abort rejects caller"| A4["finally: unlock on work.then"] A4 --> A5["next call waits<br/>until native settles"] endflowchart LR subgraph api [awaitWithSignal] Factory["() => Promise<br/>start only if not aborted"] Started["Promise<br/>keep alive after abort"] end Queue["wait for previous tail"] --> Factory InFlight["race in-flight provider work"] --> Started Started --> Unlock["unlock when settled"]Covered by client tests: aborted in-flight
start,process.run, andfilesystem.readFilekeepmaximumActive === 1until release.Docs site
/docs/processguide with provider-specific tables via<ForProvider>/kitprefix; Copy / View markdown / Open in AI target prerendered.mdfilesExamples
20 runnable projects under
examples/{go,typescript}/{daytona,modal}/process/:run-*-commandrun-*-shell-commandrun-*-with-cwdrun-*-with-envrun-*-check-exitTest plan
sdks/go/sandbox,sandbox/tests, Daytona + Modal providers (clone, timeouts, process)timeoutMs, Daytona process mapping)/docs/process, provider switch, landing, and markdown actions after Pages deploy