From 498f60290d2195695122f95d7f8c56aa3ca1b0d7 Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Mon, 24 Aug 2026 18:40:08 +0300 Subject: [PATCH 1/2] docs(box): live sessions and the new filesystem operations Documents what shipped with exec.session and the filesystem release. Adds a Live Sessions page covering the interaction model that exec.command cannot express: starting a process and holding it open, writing to stdin and closing it so an EOF-reading command finishes, allocating a real terminal with a size, signalling the process tree, and the ownership rule that closing the handle or losing the connection stops the command. Includes the Node-only restriction, a table for choosing between command, stream, and session, and worked examples for driving a REPL, answering an installer prompt, and stopping a build that overruns. Extends the Filesystem page with ranged reads, stat (including follow and the version token for optimistic concurrency), mkdir with parents, rename, and remove with recursive. Every snippet on both pages was run against a real box in both SDKs. The new examples use workspace-relative paths, which resolve, rather than the /work prefix used elsewhere on these pages, which does not exist in a box. --- box/overall/files.mdx | 112 +++++++++++ box/overall/live-sessions.mdx | 360 ++++++++++++++++++++++++++++++++++ box/overall/shell.mdx | 24 +++ docs.json | 1 + 4 files changed, 497 insertions(+) create mode 100644 box/overall/live-sessions.mdx diff --git a/box/overall/files.mdx b/box/overall/files.mdx index 224218f81..defc5d04e 100644 --- a/box/overall/files.mdx +++ b/box/overall/files.mdx @@ -74,6 +74,118 @@ print(json.loads(content)) --- +### Read part of a file + +Pass `length` to read a bounded byte range instead of the whole file, starting at `offset` (default `0`). This keeps a large log or dataset out of memory when you only need a slice of it. The server rejects a length above 8 MiB. + + +```typescript box.ts +// First 512 bytes +const head = await box.files.read("train.csv", { length: 512 }) + +// 1 KB starting at byte 4096 +const chunk = await box.files.read("train.csv", { offset: 4096, length: 1024 }) +``` + +```python box.py +# First 512 bytes +head = box.files.read("train.csv", length=512) + +# 1 KB starting at byte 4096 +chunk = box.files.read("train.csv", offset=4096, length=1024) +``` + + +--- + +### Inspect a path + +Get metadata for a single path without reading it: the entry type, size, last modified time, inode, and an opaque `version` token. + +Use `version` for optimistic concurrency. Read it before a slow operation, then compare it afterwards to detect whether the file changed underneath you. Compare it for equality only, and do not parse it. + + +```typescript box.ts +const info = await box.files.stat("report.csv") + +console.log(info.type) // "file" | "directory" | "symlink" | "other" +console.log(info.size) // 1024 +console.log(info.version) // opaque freshness token +``` + +```python box.py +info = box.files.stat("report.csv") + +print(info.type) # "file" | "directory" | "symlink" | "other" +print(info.size) # 1024 +print(info.version) # opaque freshness token +``` + + +By default a symlink is reported as `"symlink"`. Pass `follow` to resolve it and describe the target instead. + + +```typescript box.ts +const target = await box.files.stat("latest", { follow: true }) +``` + +```python box.py +target = box.files.stat("latest", follow=True) +``` + + +--- + +### Create directories + +Create a directory. Pass `parents` to create missing parent directories, the way `mkdir -p` does, and to succeed when the directory already exists. + + +```typescript box.ts +await box.files.mkdir("output/reports", { parents: true }) +``` + +```python box.py +box.files.mkdir("output/reports", parents=True) +``` + + +--- + +### Move and rename + +Move a path to a new location, which is also how you rename it. + + +```typescript box.ts +await box.files.rename("draft.md", "final.md") +``` + +```python box.py +box.files.rename("draft.md", "final.md") +``` + + +--- + +### Delete files + +Remove a file. Removing a directory requires `recursive`, so a directory is never deleted by accident. + + +```typescript box.ts +await box.files.remove("scratch.tmp") +await box.files.remove("build", { recursive: true }) +``` + +```python box.py +box.files.remove("scratch.tmp") +box.files.remove("build", recursive=True) +``` + + +--- + ### List files List the entries in a directory. Each entry includes the path, size, type, and last modified timestamp. diff --git a/box/overall/live-sessions.mdx b/box/overall/live-sessions.mdx new file mode 100644 index 000000000..720b946d3 --- /dev/null +++ b/box/overall/live-sessions.mdx @@ -0,0 +1,360 @@ +--- +title: "Live Sessions" +--- + +`box.exec.command` resolves once a command has finished. A live session resolves as soon as the command *starts*, and hands you a handle to the running process. + +That lets you do things a one-shot command cannot: write to stdin while the process runs, drive an interactive program through a real terminal, stream output as it is produced, and signal the process tree. + + + Live sessions are a Node.js API in the JavaScript SDK. Authentication travels in a request header, and browsers cannot set headers on a WebSocket handshake. + + +--- + +## API + +### Start a session + +Pass `argv` to run a program directly with no shell involved, which is the safest option when any part of the command comes from user input. + +Output arrives through callbacks as the process produces it. `stdout` and `stderr` stay separate. + + +```typescript box.ts +let out = "" + +const session = await box.exec.session({ + argv: ["npm", "test"], + onStdout: (data) => (out += Buffer.from(data).toString()), + onStderr: (data) => console.error(Buffer.from(data).toString()), +}) + +console.log(session.pid) // in-box process id +const code = await session.wait() +console.log(code) // exit code +``` + +```python box.py +chunks = [] + +session = box.exec.session( + argv=["npm", "test"], + on_stdout=chunks.append, + on_stderr=lambda data: print(data.decode(), end=""), +) + +print(session.pid) # in-box process id +code = session.wait(300) # seconds to wait +print(code) # exit code +``` + + +Use `cmd` instead of `argv` when you want a shell, for pipes, globs, or `&&`. It runs through `bash -lc`. + + +```typescript box.ts +const session = await box.exec.session({ cmd: "cat *.log | grep ERROR" }) +``` + +```python box.py +session = box.exec.session(cmd="cat *.log | grep ERROR") +``` + + + + `argv` does not expand variables or treat `;` as a separator, so `argv: ["echo", "$HOME; rm -rf /"]` prints that text literally. Prefer it over `cmd` for untrusted input. + + +--- + +### Write to stdin + +Send input to the running process. Close stdin when you are done so a command that reads to end of input can finish. + + +```typescript box.ts +let out = "" + +const session = await box.exec.session({ + argv: ["sort"], + onStdout: (data) => (out += Buffer.from(data).toString()), +}) + +session.write("banana\napple\n") +session.endStdin() // EOF, so sort can finish + +await session.wait() +console.log(out) // "apple\nbanana\n" +``` + +```python box.py +chunks = [] + +session = box.exec.session(argv=["sort"], on_stdout=chunks.append) + +session.write("banana\napple\n") +session.end_stdin() # EOF, so sort can finish + +session.wait(30) +print(b"".join(chunks).decode()) # "apple\nbanana\n" +``` + + +A long-lived process can take many rounds of input without ever closing stdin. + +--- + +### Run interactive programs + +Set `tty` to allocate a real terminal. Programs that behave differently when piped, such as REPLs, `top`, or anything drawing a terminal UI, then work as they do in a real shell. Give the terminal a size with `rows` and `cols`, and change it later with `resize`. + +With a TTY, stderr is merged into stdout, the same as in a terminal. + + +```typescript box.ts +const repl = await box.exec.session({ + cmd: "python3 -i", + tty: true, + rows: 24, + cols: 80, + onStdout: (data) => process.stdout.write(Buffer.from(data)), +}) + +repl.write("2 + 2\n") +repl.resize(50, 120) +``` + +```python box.py +repl = box.exec.session( + cmd="python3 -i", + tty=True, + rows=24, + cols=80, + on_stdout=lambda data: print(data.decode(), end=""), +) + +repl.write("2 + 2\n") +repl.resize(50, 120) +``` + + +--- + +### Set the directory and environment + +`cwd` places the process, resolving against the box's current directory. `env` entries are `KEY=VALUE` strings overlaid on the box environment. + + +```typescript box.ts +const session = await box.exec.session({ + argv: ["npm", "run", "build"], + cwd: "packages/web", + env: ["NODE_ENV=production"], +}) +``` + +```python box.py +session = box.exec.session( + argv=["npm", "run", "build"], + cwd="packages/web", + env=["NODE_ENV=production"], +) +``` + + + + A few environment variables are reserved by the runtime and are dropped rather than applied, so a session cannot use them to alter how the box itself runs. + + +--- + +### Stop a session + +`terminate` asks the server for a graceful stop: SIGTERM now, then SIGKILL after the grace period if the process is still running. + + +```typescript box.ts +session.terminate(5000) // SIGTERM, then SIGKILL after 5s +await session.wait() +``` + +```python box.py +session.terminate(5000) # SIGTERM, then SIGKILL after 5s +session.wait(30) +``` + + + + Only the first `terminate` starts the sequence. Later calls are ignored, so the grace period cannot be changed once it is running. Send `KILL` to stop the process immediately instead. + + +`kill` sends a single signal to the whole process tree, so background children started by the command are signalled too. It defaults to `TERM`, and accepts `TERM`, `KILL`, `INT`, `HUP`, `TSTP`, `QUIT`, `USR1`, and `USR2`. + + +```typescript box.ts +session.kill() // TERM +session.kill("KILL") // stop immediately +``` + +```python box.py +session.kill() # TERM +session.kill("KILL") # stop immediately +``` + + +--- + +## The session owns the process + +A session is a live connection, and the process belongs to it. Closing the handle, losing the network link, or exiting your program all stop the command rather than leaving it running in the box. + + + Sessions cannot be reattached. Once the connection is gone the process is gone with it, so a session is the wrong tool for work that must outlive your program. Use [schedules](/box/overall/schedules) or a `keep_alive` box with `box.exec.command` for that. + + +Both SDKs expose this as a context manager, which stops the process on the way out even if your code raises. + + +```typescript box.ts +const dev = await box.exec.session({ cmd: "npm run dev", tty: true }) +try { + dev.write("rs\n") // restart +} finally { + dev.close() // stops the process +} +``` + +```python box.py +with box.exec.session(cmd="npm run dev", tty=True) as dev: + dev.write("rs\n") # restart +# the process is stopped on the way out +``` + + +--- + +## Sessions or commands? + +| Use | When | +| --- | --- | +| `box.exec.command` | You want the result of a command that finishes on its own. | +| `box.exec.stream` | You want output as it arrives, but no input and no signals. | +| `box.exec.session` | You need stdin, a terminal, signals, or a process you hold open. | + +--- + +## Examples + +### Drive a REPL and collect answers + + +```typescript box.ts +let out = "" +const repl = await box.exec.session({ + argv: ["python3", "-i", "-q"], + onStdout: (data) => (out += Buffer.from(data).toString()), +}) + +for (const expr of ["import math", "math.factorial(10)", "sum(range(100))"]) { + repl.write(`${expr}\n`) + await new Promise((r) => setTimeout(r, 200)) +} + +repl.endStdin() +await repl.wait() +console.log(out) +``` + +```python box.py +import time + +chunks = [] +repl = box.exec.session(argv=["python3", "-i", "-q"], on_stdout=chunks.append) + +for expr in ["import math", "math.factorial(10)", "sum(range(100))"]: + repl.write(f"{expr}\n") + time.sleep(0.2) + +repl.end_stdin() +repl.wait(30) +print(b"".join(chunks).decode()) +``` + + +--- + +### Answer a prompt from an installer + +Collect the output as it arrives, wait for the prompt to show up, then answer it. + + +```typescript box.ts +let out = "" + +const session = await box.exec.session({ + cmd: "npm create vite@latest my-app", + tty: true, + onStdout: (data) => (out += Buffer.from(data).toString()), +}) + +const deadline = Date.now() + 30000 +while (!out.includes("Select a framework") && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 100)) +} +session.write("\n") // accept the default + +await session.wait() +``` + +```python box.py +import time + +out = "" + +def collect(data: bytes) -> None: + global out + out += data.decode() + +session = box.exec.session( + cmd="npm create vite@latest my-app", + tty=True, + on_stdout=collect, +) + +deadline = time.monotonic() + 30 +while "Select a framework" not in out and time.monotonic() < deadline: + time.sleep(0.1) +session.write("\n") # accept the default + +session.wait(300) +``` + + +--- + +### Stop a build that runs too long + + +```typescript box.ts +const build = await box.exec.session({ argv: ["npm", "run", "build"] }) + +const timer = setTimeout(() => build.terminate(5000), 60000) +const code = await build.wait() +clearTimeout(timer) + +console.log(code === 0 ? "built" : `stopped with ${code}`) +``` + +```python box.py +build = box.exec.session(argv=["npm", "run", "build"]) + +try: + code = build.wait(60) +except TimeoutError: + build.terminate(5000) + code = build.wait(30) + +print("built" if code == 0 else f"stopped with {code}") +``` + diff --git a/box/overall/shell.mdx b/box/overall/shell.mdx index 9536ee1ce..4effafc70 100644 --- a/box/overall/shell.mdx +++ b/box/overall/shell.mdx @@ -132,6 +132,30 @@ print(run.result) --- +### Hold a command open + +`exec.command` runs a command to completion. When you need to write to stdin, drive an interactive program through a terminal, or signal a process while it runs, start a [live session](/box/overall/live-sessions) instead. + + +```typescript box.ts +const session = await box.exec.session({ argv: ["sort"] }) +session.write("banana\napple\n") +session.endStdin() + +await session.wait() +``` + +```python box.py +session = box.exec.session(argv=["sort"]) +session.write("banana\napple\n") +session.end_stdin() + +session.wait(30) +``` + + +--- + ### Cancel a long-running command You can cancel a run to abort it. The status becomes `"cancelled"`. diff --git a/docs.json b/docs.json index 2b79c5a30..9798d4924 100644 --- a/docs.json +++ b/docs.json @@ -2049,6 +2049,7 @@ }, "box/overall/git", "box/overall/shell", + "box/overall/live-sessions", "box/overall/files", "box/overall/snapshots" ] From 05a1e10687243576f48e0bcbe4a9cd8cf241989c Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Mon, 24 Aug 2026 18:50:23 +0300 Subject: [PATCH 2/2] docs(box): correct live-session teardown, units, and the ephemeral matrix Addresses review on DX-2953. The teardown section claimed both SDKs expose a context manager. Only Python does; the JavaScript handle has no Symbol.dispose, so the try/finally in its example was right and the prose was wrong. The terminate note said to "send KILL", which reads as an argument to terminate. It now names kill("KILL"), matching the snippet below it. Timeout units were mixed: Python wait() takes seconds while terminate() takes milliseconds, so wait(300) next to terminate(5000) invited a copy-paste error. Examples now use wait() with no argument as the analogue of the JavaScript call, every remaining timeout is labelled with its unit, and the difference between the two wait() signatures is stated once up front. The EphemeralBox capability matrix predated both releases. EphemeralBox forwards exec and files wholesale, so it already has exec.session and stat/mkdir/rename/ remove; verified against a real ephemeral box rather than inferred from the forwarding. Also links the how-it-works Shell row to the new page, widens its Filesystem row now that files does more than transfer, and regenerates llms.txt and llms-full.txt so CI does not have to commit them. --- box/overall/ephemeral-box.mdx | 2 + box/overall/how-it-works.mdx | 4 +- box/overall/live-sessions.mdx | 29 +- llms-full.txt | 505 +++++++++++++++++++++++++++++++++- llms.txt | 1 + 5 files changed, 524 insertions(+), 17 deletions(-) diff --git a/box/overall/ephemeral-box.mdx b/box/overall/ephemeral-box.mdx index 0c6263fb4..658204d9c 100644 --- a/box/overall/ephemeral-box.mdx +++ b/box/overall/ephemeral-box.mdx @@ -46,7 +46,9 @@ The request sends `{ ephemeral: true, ttl?, runtime? }` to `POST /v2/box`. | `exec.code()` | Yes | Yes | | `exec.stream()` | Yes | Yes | | `exec.streamCode()` | Yes | Yes | +| `exec.session()` | Yes | Yes | | `files.read/write/list/upload/download` | Yes | Yes | +| `files.stat/mkdir/rename/remove` | Yes | Yes | | `schedule.exec/prompt/list/get/pause/resume/delete` | Yes | Yes | | `cd()` / `cwd` | Yes | Yes | | `getStatus()` | Yes | Yes | diff --git a/box/overall/how-it-works.mdx b/box/overall/how-it-works.mdx index 2f51dd77b..b6a1fde84 100644 --- a/box/overall/how-it-works.mdx +++ b/box/overall/how-it-works.mdx @@ -18,8 +18,8 @@ Every box is a self-contained environment with five capabilities: | -------------- | ------------------------------------------------------------ | | **Agent** | Run a coding agent (Claude Code or Codex) | | **Git** | Clone repos, inspect diffs, and open pull requests | -| **Shell** | Execute OS-level commands directly | -| **Filesystem** | Upload, write, read, list, and download files inside the box | +| **Shell** | Execute OS-level commands, or hold one open with [live sessions](/box/overall/live-sessions) | +| **Filesystem** | Upload, write, read, list, download, and manage files inside the box | | **Snapshots** | Capture box state and restore new boxes from it | The agent has full access to the shell, filesystem, and git inside its box. It can install packages, write files, run tests, and interact with the network. diff --git a/box/overall/live-sessions.mdx b/box/overall/live-sessions.mdx index 720b946d3..9052f37d7 100644 --- a/box/overall/live-sessions.mdx +++ b/box/overall/live-sessions.mdx @@ -45,11 +45,13 @@ session = box.exec.session( ) print(session.pid) # in-box process id -code = session.wait(300) # seconds to wait +code = session.wait() print(code) # exit code ``` +`wait()` blocks until the process exits and returns its exit code. In Python you can pass a timeout in **seconds** to bound the wait, and it raises `TimeoutError` if that elapses. The JavaScript `wait()` takes no timeout, so cap it with your own timer if you need one. + Use `cmd` instead of `argv` when you want a shell, for pipes, globs, or `&&`. It runs through `bash -lc`. @@ -96,7 +98,7 @@ session = box.exec.session(argv=["sort"], on_stdout=chunks.append) session.write("banana\napple\n") session.end_stdin() # EOF, so sort can finish -session.wait(30) +session.wait() print(b"".join(chunks).decode()) # "apple\nbanana\n" ``` @@ -175,18 +177,18 @@ session = box.exec.session( ```typescript box.ts -session.terminate(5000) // SIGTERM, then SIGKILL after 5s +session.terminate(5000) // milliseconds: SIGTERM, then SIGKILL after 5s await session.wait() ``` ```python box.py -session.terminate(5000) # SIGTERM, then SIGKILL after 5s -session.wait(30) +session.terminate(5000) # milliseconds: SIGTERM, then SIGKILL after 5s +session.wait() ``` - Only the first `terminate` starts the sequence. Later calls are ignored, so the grace period cannot be changed once it is running. Send `KILL` to stop the process immediately instead. + Only the first `terminate` starts the sequence. Later calls are ignored, so the grace period cannot be changed once it is running. Use `kill("KILL")` to stop the process immediately instead. `kill` sends a single signal to the whole process tree, so background children started by the command are signalled too. It defaults to `TERM`, and accepts `TERM`, `KILL`, `INT`, `HUP`, `TSTP`, `QUIT`, `USR1`, and `USR2`. @@ -213,7 +215,7 @@ A session is a live connection, and the process belongs to it. Closing the handl Sessions cannot be reattached. Once the connection is gone the process is gone with it, so a session is the wrong tool for work that must outlive your program. Use [schedules](/box/overall/schedules) or a `keep_alive` box with `box.exec.command` for that. -Both SDKs expose this as a context manager, which stops the process on the way out even if your code raises. +Always stop the session on your way out, including when your code raises. In Python the handle is a context manager. In JavaScript, close it in a `finally` block. ```typescript box.ts @@ -277,7 +279,7 @@ for expr in ["import math", "math.factorial(10)", "sum(range(100))"]: time.sleep(0.2) repl.end_stdin() -repl.wait(30) +repl.wait() print(b"".join(chunks).decode()) ``` @@ -327,7 +329,7 @@ while "Select a framework" not in out and time.monotonic() < deadline: time.sleep(0.1) session.write("\n") # accept the default -session.wait(300) +session.wait() ``` @@ -339,7 +341,8 @@ session.wait(300) ```typescript box.ts const build = await box.exec.session({ argv: ["npm", "run", "build"] }) -const timer = setTimeout(() => build.terminate(5000), 60000) +// wait() has no timeout in JS, so cap the build with a timer. +const timer = setTimeout(() => build.terminate(5000), 60000) // ms const code = await build.wait() clearTimeout(timer) @@ -350,10 +353,10 @@ console.log(code === 0 ? "built" : `stopped with ${code}`) build = box.exec.session(argv=["npm", "run", "build"]) try: - code = build.wait(60) + code = build.wait(60) # seconds except TimeoutError: - build.terminate(5000) - code = build.wait(30) + build.terminate(5000) # milliseconds + code = build.wait() print("built" if code == 0 else f"stopped with {code}") ``` diff --git a/llms-full.txt b/llms-full.txt index 0c86faff4..249a685e4 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -3816,7 +3816,9 @@ The request sends `{ ephemeral: true, ttl?, runtime? }` to `POST /v2/box`. | `exec.code()` | Yes | Yes | | `exec.stream()` | Yes | Yes | | `exec.streamCode()` | Yes | Yes | +| `exec.session()` | Yes | Yes | | `files.read/write/list/upload/download` | Yes | Yes | +| `files.stat/mkdir/rename/remove` | Yes | Yes | | `schedule.exec/prompt/list/get/pause/resume/delete` | Yes | Yes | | `cd()` / `cwd` | Yes | Yes | | `getStatus()` | Yes | Yes | @@ -3995,6 +3997,118 @@ print(json.loads(content)) *** +### Read part of a file + +Pass `length` to read a bounded byte range instead of the whole file, starting at `offset` (default `0`). This keeps a large log or dataset out of memory when you only need a slice of it. The server rejects a length above 8 MiB. + + +```typescript box.ts +// First 512 bytes +const head = await box.files.read("train.csv", { length: 512 }) + +// 1 KB starting at byte 4096 +const chunk = await box.files.read("train.csv", { offset: 4096, length: 1024 }) +``` + +```python box.py +# First 512 bytes +head = box.files.read("train.csv", length=512) + +# 1 KB starting at byte 4096 +chunk = box.files.read("train.csv", offset=4096, length=1024) +``` + + +*** + +### Inspect a path + +Get metadata for a single path without reading it: the entry type, size, last modified time, inode, and an opaque `version` token. + +Use `version` for optimistic concurrency. Read it before a slow operation, then compare it afterwards to detect whether the file changed underneath you. Compare it for equality only, and do not parse it. + + +```typescript box.ts +const info = await box.files.stat("report.csv") + +console.log(info.type) // "file" | "directory" | "symlink" | "other" +console.log(info.size) // 1024 +console.log(info.version) // opaque freshness token +``` + +```python box.py +info = box.files.stat("report.csv") + +print(info.type) # "file" | "directory" | "symlink" | "other" +print(info.size) # 1024 +print(info.version) # opaque freshness token +``` + + +By default a symlink is reported as `"symlink"`. Pass `follow` to resolve it and describe the target instead. + + +```typescript box.ts +const target = await box.files.stat("latest", { follow: true }) +``` + +```python box.py +target = box.files.stat("latest", follow=True) +``` + + +*** + +### Create directories + +Create a directory. Pass `parents` to create missing parent directories, the way `mkdir -p` does, and to succeed when the directory already exists. + + +```typescript box.ts +await box.files.mkdir("output/reports", { parents: true }) +``` + +```python box.py +box.files.mkdir("output/reports", parents=True) +``` + + +*** + +### Move and rename + +Move a path to a new location, which is also how you rename it. + + +```typescript box.ts +await box.files.rename("draft.md", "final.md") +``` + +```python box.py +box.files.rename("draft.md", "final.md") +``` + + +*** + +### Delete files + +Remove a file. Removing a directory requires `recursive`, so a directory is never deleted by accident. + + +```typescript box.ts +await box.files.remove("scratch.tmp") +await box.files.remove("build", { recursive: true }) +``` + +```python box.py +box.files.remove("scratch.tmp") +box.files.remove("build", recursive=True) +``` + + +*** + ### List files List the entries in a directory. Each entry includes the path, size, type, and last modified timestamp. @@ -4804,8 +4918,8 @@ Every box is a self-contained environment with five capabilities: | -------------- | ------------------------------------------------------------ | | **Agent** | Run a coding agent (Claude Code or Codex) | | **Git** | Clone repos, inspect diffs, and open pull requests | -| **Shell** | Execute OS-level commands directly | -| **Filesystem** | Upload, write, read, list, and download files inside the box | +| **Shell** | Execute OS-level commands, or hold one open with [live sessions](/docs/box/overall/live-sessions) | +| **Filesystem** | Upload, write, read, list, download, and manage files inside the box | | **Snapshots** | Capture box state and restore new boxes from it | The agent has full access to the shell, filesystem, and git inside its box. It can install packages, write files, run tests, and interact with the network. @@ -5255,6 +5369,369 @@ Use keep-alive when the box itself needs to stay available between requests: Avoid keep-alive when you only need a reusable workspace. In those cases, the default box lifecycle plus [Snapshots](/docs/box/overall/snapshots) is usually enough. +# Live Sessions +Source: https://upstash.com/docs/box/overall/live-sessions + +`box.exec.command` resolves once a command has finished. A live session resolves as soon as the command *starts*, and hands you a handle to the running process. + +That lets you do things a one-shot command cannot: write to stdin while the process runs, drive an interactive program through a real terminal, stream output as it is produced, and signal the process tree. + + + Live sessions are a Node.js API in the JavaScript SDK. Authentication travels in a request header, and browsers cannot set headers on a WebSocket handshake. + + +*** + +## API + +### Start a session + +Pass `argv` to run a program directly with no shell involved, which is the safest option when any part of the command comes from user input. + +Output arrives through callbacks as the process produces it. `stdout` and `stderr` stay separate. + + +```typescript box.ts +let out = "" + +const session = await box.exec.session({ + argv: ["npm", "test"], + onStdout: (data) => (out += Buffer.from(data).toString()), + onStderr: (data) => console.error(Buffer.from(data).toString()), +}) + +console.log(session.pid) // in-box process id +const code = await session.wait() +console.log(code) // exit code +``` + +```python box.py +chunks = [] + +session = box.exec.session( + argv=["npm", "test"], + on_stdout=chunks.append, + on_stderr=lambda data: print(data.decode(), end=""), +) + +print(session.pid) # in-box process id +code = session.wait() +print(code) # exit code +``` + + +`wait()` blocks until the process exits and returns its exit code. In Python you can pass a timeout in **seconds** to bound the wait, and it raises `TimeoutError` if that elapses. The JavaScript `wait()` takes no timeout, so cap it with your own timer if you need one. + +Use `cmd` instead of `argv` when you want a shell, for pipes, globs, or `&&`. It runs through `bash -lc`. + + +```typescript box.ts +const session = await box.exec.session({ cmd: "cat *.log | grep ERROR" }) +``` + +```python box.py +session = box.exec.session(cmd="cat *.log | grep ERROR") +``` + + + + `argv` does not expand variables or treat `;` as a separator, so `argv: ["echo", "$HOME; rm -rf /"]` prints that text literally. Prefer it over `cmd` for untrusted input. + + +*** + +### Write to stdin + +Send input to the running process. Close stdin when you are done so a command that reads to end of input can finish. + + +```typescript box.ts +let out = "" + +const session = await box.exec.session({ + argv: ["sort"], + onStdout: (data) => (out += Buffer.from(data).toString()), +}) + +session.write("banana\napple\n") +session.endStdin() // EOF, so sort can finish + +await session.wait() +console.log(out) // "apple\nbanana\n" +``` + +```python box.py +chunks = [] + +session = box.exec.session(argv=["sort"], on_stdout=chunks.append) + +session.write("banana\napple\n") +session.end_stdin() # EOF, so sort can finish + +session.wait() +print(b"".join(chunks).decode()) # "apple\nbanana\n" +``` + + +A long-lived process can take many rounds of input without ever closing stdin. + +*** + +### Run interactive programs + +Set `tty` to allocate a real terminal. Programs that behave differently when piped, such as REPLs, `top`, or anything drawing a terminal UI, then work as they do in a real shell. Give the terminal a size with `rows` and `cols`, and change it later with `resize`. + +With a TTY, stderr is merged into stdout, the same as in a terminal. + + +```typescript box.ts +const repl = await box.exec.session({ + cmd: "python3 -i", + tty: true, + rows: 24, + cols: 80, + onStdout: (data) => process.stdout.write(Buffer.from(data)), +}) + +repl.write("2 + 2\n") +repl.resize(50, 120) +``` + +```python box.py +repl = box.exec.session( + cmd="python3 -i", + tty=True, + rows=24, + cols=80, + on_stdout=lambda data: print(data.decode(), end=""), +) + +repl.write("2 + 2\n") +repl.resize(50, 120) +``` + + +*** + +### Set the directory and environment + +`cwd` places the process, resolving against the box's current directory. `env` entries are `KEY=VALUE` strings overlaid on the box environment. + + +```typescript box.ts +const session = await box.exec.session({ + argv: ["npm", "run", "build"], + cwd: "packages/web", + env: ["NODE_ENV=production"], +}) +``` + +```python box.py +session = box.exec.session( + argv=["npm", "run", "build"], + cwd="packages/web", + env=["NODE_ENV=production"], +) +``` + + + + A few environment variables are reserved by the runtime and are dropped rather than applied, so a session cannot use them to alter how the box itself runs. + + +*** + +### Stop a session + +`terminate` asks the server for a graceful stop: SIGTERM now, then SIGKILL after the grace period if the process is still running. + + +```typescript box.ts +session.terminate(5000) // milliseconds: SIGTERM, then SIGKILL after 5s +await session.wait() +``` + +```python box.py +session.terminate(5000) # milliseconds: SIGTERM, then SIGKILL after 5s +session.wait() +``` + + + + Only the first `terminate` starts the sequence. Later calls are ignored, so the grace period cannot be changed once it is running. Use `kill("KILL")` to stop the process immediately instead. + + +`kill` sends a single signal to the whole process tree, so background children started by the command are signalled too. It defaults to `TERM`, and accepts `TERM`, `KILL`, `INT`, `HUP`, `TSTP`, `QUIT`, `USR1`, and `USR2`. + + +```typescript box.ts +session.kill() // TERM +session.kill("KILL") // stop immediately +``` + +```python box.py +session.kill() # TERM +session.kill("KILL") # stop immediately +``` + + +*** + +## The session owns the process + +A session is a live connection, and the process belongs to it. Closing the handle, losing the network link, or exiting your program all stop the command rather than leaving it running in the box. + + + Sessions cannot be reattached. Once the connection is gone the process is gone with it, so a session is the wrong tool for work that must outlive your program. Use [schedules](/docs/box/overall/schedules) or a `keep_alive` box with `box.exec.command` for that. + + +Always stop the session on your way out, including when your code raises. In Python the handle is a context manager. In JavaScript, close it in a `finally` block. + + +```typescript box.ts +const dev = await box.exec.session({ cmd: "npm run dev", tty: true }) +try { + dev.write("rs\n") // restart +} finally { + dev.close() // stops the process +} +``` + +```python box.py +with box.exec.session(cmd="npm run dev", tty=True) as dev: + dev.write("rs\n") # restart +# the process is stopped on the way out +``` + + +*** + +## Sessions or commands? + +| Use | When | +| --- | --- | +| `box.exec.command` | You want the result of a command that finishes on its own. | +| `box.exec.stream` | You want output as it arrives, but no input and no signals. | +| `box.exec.session` | You need stdin, a terminal, signals, or a process you hold open. | + +*** + +## Examples + +### Drive a REPL and collect answers + + +```typescript box.ts +let out = "" +const repl = await box.exec.session({ + argv: ["python3", "-i", "-q"], + onStdout: (data) => (out += Buffer.from(data).toString()), +}) + +for (const expr of ["import math", "math.factorial(10)", "sum(range(100))"]) { + repl.write(`${expr}\n`) + await new Promise((r) => setTimeout(r, 200)) +} + +repl.endStdin() +await repl.wait() +console.log(out) +``` + +```python box.py +import time + +chunks = [] +repl = box.exec.session(argv=["python3", "-i", "-q"], on_stdout=chunks.append) + +for expr in ["import math", "math.factorial(10)", "sum(range(100))"]: + repl.write(f"{expr}\n") + time.sleep(0.2) + +repl.end_stdin() +repl.wait() +print(b"".join(chunks).decode()) +``` + + +*** + +### Answer a prompt from an installer + +Collect the output as it arrives, wait for the prompt to show up, then answer it. + + +```typescript box.ts +let out = "" + +const session = await box.exec.session({ + cmd: "npm create vite@latest my-app", + tty: true, + onStdout: (data) => (out += Buffer.from(data).toString()), +}) + +const deadline = Date.now() + 30000 +while (!out.includes("Select a framework") && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 100)) +} +session.write("\n") // accept the default + +await session.wait() +``` + +```python box.py +import time + +out = "" + +def collect(data: bytes) -> None: + global out + out += data.decode() + +session = box.exec.session( + cmd="npm create vite@latest my-app", + tty=True, + on_stdout=collect, +) + +deadline = time.monotonic() + 30 +while "Select a framework" not in out and time.monotonic() < deadline: + time.sleep(0.1) +session.write("\n") # accept the default + +session.wait() +``` + + +*** + +### Stop a build that runs too long + + +```typescript box.ts +const build = await box.exec.session({ argv: ["npm", "run", "build"] }) + +// wait() has no timeout in JS, so cap the build with a timer. +const timer = setTimeout(() => build.terminate(5000), 60000) // ms +const code = await build.wait() +clearTimeout(timer) + +console.log(code === 0 ? "built" : `stopped with ${code}`) +``` + +```python box.py +build = box.exec.session(argv=["npm", "run", "build"]) + +try: + code = build.wait(60) # seconds +except TimeoutError: + build.terminate(5000) # milliseconds + code = build.wait() + +print("built" if code == 0 else f"stopped with {code}") +``` + + # Network Policy Source: https://upstash.com/docs/box/overall/network-policy @@ -6926,6 +7403,30 @@ print(run.result) *** +### Hold a command open + +`exec.command` runs a command to completion. When you need to write to stdin, drive an interactive program through a terminal, or signal a process while it runs, start a [live session](/docs/box/overall/live-sessions) instead. + + +```typescript box.ts +const session = await box.exec.session({ argv: ["sort"] }) +session.write("banana\napple\n") +session.endStdin() + +await session.wait() +``` + +```python box.py +session = box.exec.session(argv=["sort"]) +session.write("banana\napple\n") +session.end_stdin() + +session.wait(30) +``` + + +*** + ### Cancel a long-running command You can cancel a run to abort it. The status becomes `"cancelled"`. diff --git a/llms.txt b/llms.txt index c198a10f7..2760cdf7d 100644 --- a/llms.txt +++ b/llms.txt @@ -67,6 +67,7 @@ - [Git](https://upstash.com/docs/box/overall/git.md) - [Box Basics](https://upstash.com/docs/box/overall/how-it-works.md) - [Keep Alive](https://upstash.com/docs/box/overall/keep-alive.md) +- [Live Sessions](https://upstash.com/docs/box/overall/live-sessions.md) - [Network Policy](https://upstash.com/docs/box/overall/network-policy.md) - [Public URLs](https://upstash.com/docs/box/overall/preview.md) - [Pricing & Limits](https://upstash.com/docs/box/overall/pricing.md)