# Getting Started with DynoCap Install DynoCap on Apple Silicon with Homebrew or curl, prepare a project, and run a command in an isolated Linux microVM. HTML: https://dynocap.dev/docs/tutorials/getting-started Install DynoCap on an Apple Silicon Mac, prepare one project, and run a command inside an isolated Linux guest. You will: 1. Install the CLI with Homebrew, or with curl. 2. Check your setup with `dynocap doctor`. 3. Repair it with `dynocap doctor --fix`. 4. Launch a raw sandbox and execute commands over vsock. 5. Safely destroy the sandbox. --- ## Prerequisites - **Hardware:** Apple Silicon Mac (M1 or newer). Intel Macs are refused by the installers. - **Operating System:** macOS 13 (Ventura) or newer. - **Homebrew**, for the primary install. The formula also installs `e2fsprogs`, which provides `mke2fs`. That formula is keg-only; DynoCap finds it at `/opt/homebrew/opt/e2fsprogs/sbin/mke2fs`. - **Go 1.22+** (`brew install go`) the first time a runtime is prepared. The guest image ships in the release archive, so a Homebrew or curl install does not need Go to copy it. Swift and Node are for a source build and for this repository's TypeScript packages. --- ## Step 1: Install DynoCap uses Apple's `Virtualization.framework`, which requires the `com.apple.security.virtualization` entitlement. There is no Apple Developer ID signature. Both installers re-sign the CLI on your Mac with `codesign --sign -` and the bundled entitlement. Neither installer uses a Homebrew bottle, because a poured bottle would skip that re-sign. ### Homebrew ```sh brew install baires/tap/dynocap ``` That installs the formula from [baires/homebrew-tap](https://github.com/baires/homebrew-tap). On Homebrew 6, this fully qualified name trusts that formula. Trust it explicitly when you want later commands to use the short name `dynocap`, such as `brew upgrade dynocap`: ```sh brew trust --formula baires/tap/dynocap ``` The formula downloads the versioned release archive, checks its sha256, and re-signs `dynocap`. Then run doctor in your project, which the formula also prints as a caveat: ```sh dynocap doctor --fix ``` ### Curl ```sh curl -fsSL https://dynocap.dev/install.sh | sh ``` The site serves [`scripts/install.sh`](https://github.com/baires/dynocap/blob/main/scripts/install.sh). The script downloads `dynocap-darwin-arm64.tar.gz` from the latest GitHub release, checks its sha256, installs it under `~/.local/dynocap`, and symlinks `~/.local/bin/dynocap`. The checksum catches a truncated download. When `gh` is installed and logged in, the script also requires a build attestation from `baires/dynocap`. Its last step is `dynocap doctor --fix`. If `mke2fs` is missing, it tells you to run `brew install e2fsprogs`. If `~/.local/bin` is not on `PATH`, it tells you to add it. ### From source A checkout has no bundled guest image. Building needs Xcode Command Line Tools (Swift 5.9+), Go 1.22+, and `mke2fs` (`brew install e2fsprogs`). ```sh ./scripts/build-macos.sh export PATH="$PWD/.build/release:$PATH" ``` Re-run `./scripts/build-macos.sh` after every rebuild so the binary stays signed. `npm ci` is for the repository's TypeScript packages, not for the CLI. --- ## Step 2: Check Your Setup Go to your project and run `dynocap doctor`. It checks the platform, code signing, companion binaries, guest image, runtime, and daemon, and changes nothing: ```sh cd ~/code/my-app dynocap doctor ``` Doctor reads your project's files to pick its runtime, the same way `clone` does: | File at the project root | Runtime | | :--------------------------------------------------------------------- | :------------ | | `package.json` | `node:24` | | `pyproject.toml`, `uv.lock`, `requirements.txt`, `setup.py`, `Pipfile` | `python:3.14` | | `go.mod` | `go:1.27` | On a fresh source build, the guest image and runtime are not built yet. A Homebrew or curl install already contains the guest image, and `--fix` copies it into your home. Each failure says what is wrong and what to run: ```text ERROR images The guest image for aarch64 has not been built yet. Next: Run dynocap doctor --fix to build the guest image (downloads pinned Alpine files; needs Go). ERROR runtime Runtime python:3.14 is not prepared for aarch64 yet. This project's pyproject.toml needs it. Next: Run dynocap doctor --fix to prepare python:3.14 (downloads verified inputs; takes a few minutes). 2 required checks failed. Run dynocap doctor --fix to repair them. ``` To prepare a different runtime, pass `--runtime`, for example `dynocap doctor --fix --runtime node:22`. Run outside a project, doctor requires no runtime. It lists the prepared ones instead, because raw sandboxes need none. --- ## Step 3: Repair It with `doctor --fix` ```sh dynocap doctor --fix ``` `--fix` copies a bundled guest image when the install has one, and builds it otherwise. It prepares your project's runtime and restarts a daemon left over from an older build. Every download is pinned and verified. It then runs the checks again and ends with `Required checks passed.` Doctor finds that prefix from the `dynocap` binary, whether it is a source checkout or a Homebrew or curl install, so you can run it in any directory. You can also name the project instead of changing directory: `dynocap doctor --fix ~/code/my-app`. > **Tip:** After you rebuild `dynocap`, run `dynocap doctor --fix` again. It restarts the daemon so it runs the new build. --- ## Step 4: Boot a Raw Sandbox A raw sandbox boots the minimal Alpine Linux image without importing a project workspace. Start a sandbox named `my-first-box`: ```sh dynocap run --name my-first-box ``` Output: ```text running my-first-box ``` Behind the scenes: 1. The `dynocap` CLI launched the per-user background daemon (`~/.dynocap/daemon.sock`). 2. The daemon booted a Linux virtual machine with 1 vCPU and 1024 MB RAM via Virtualization.framework. 3. The guest booted its initramfs in under 200ms and launched the internal Go `dynocap-agent`. 4. The agent connected back over `virtio-vsock` (guest port 5252). Verify the sandbox status: ```sh dynocap ps ``` You will see: ```text NAME/ID KIND PHASE RUNTIME NETWORK TTL RETENTION my-first-box sandbox running minimal none 29m - ``` Notice that `NETWORK` is `none`. The sandbox is completely offline. No network interface exists inside the guest. --- ## Step 5: Execute Commands Inside the Sandbox Run commands inside your guest with `dynocap exec`: ```sh dynocap exec my-first-box -- uname -a ``` Output: ```text Linux dynocap 6.18.62-0-virt #1-Alpine SMP PREEMPT_DYNAMIC aarch64 Linux ``` Test running an isolated shell command: ```sh dynocap exec my-first-box -- sh -c "echo 'Hello from DynoCap' && id" ``` Output: ```text Hello from DynoCap uid=0(root) gid=0(root) groups=0(root) ``` DynoCap streams stdout and stderr separately, and exits with the exact status code returned by the guest command. --- ## Step 6: Destroy the Sandbox When your work is done, remove the sandbox: ```sh dynocap rm my-first-box --force ``` Output: ```text removed my-first-box ``` Because `my-first-box` was actively running, `--force` explicitly confirms discarding running processes. The daemon terminates the virtual machine and cleans up runtime descriptors. --- ## Next Steps Now that the CLI is installed and a raw sandbox runs, proceed to: - **[End-to-End Agent Task Workflow](/tutorials/agent-task-workflow)**: Copy a project into a task, install from a dependency seed, and export a verified diff. The host checkout is never mounted. - **[Interactive Shells & Terminal Sessions](/how-to/interactive-shells-and-terminal)**: Learn how to attach interactive TTY sessions for live debugging. --- # End-to-End Agent Task Workflow Learn how to import a repository into an isolated Node 24 sandbox, run coding tasks, preview diffs, and safely export patches. HTML: https://dynocap.dev/docs/tutorials/agent-task-workflow This tutorial demonstrates the core developer workflow of DynoCap: importing a project into an isolated sandbox, executing autonomous coding tasks or tests, inspecting changes, and extracting a verified Git patch. By the end of this lesson, you will: 1. Prepare the verified Node 24 task runtime. 2. Clone a project into an isolated sandbox with filtered egress. 3. Install dependencies and run test suites. 4. Preview changes in real time with `dynocap diff`. 5. Finish the task, destroy the microVM, and apply the exported patch to your host repo. --- ## What is a Task? Unlike a raw sandbox, a **task** has: - A disposable Linux ext4 root filesystem containing an imported project tree located at `/workspace`. - A prepared runtime, such as `node:24`. - Automatic execution logging and diff calculation. - An exported **result directory** on the host that persists for 7 days after the sandbox is destroyed. Your host repository is **never mounted or shared**. Changes made inside the sandbox are completely isolated. --- ## Step 1: Prepare the Node 24 Runtime Project tasks require a prepared runtime disk. DynoCap uses explicit runtime preparation: it downloads verified packages and verifies hashes before publishing the artifact. In your `dynocap` source checkout, run: ```sh dynocap runtime prepare . --runtime node:24 ``` This builds and validates the Node 24 runtime disk (including Node, npm, bash, git and an SSH client) and saves it to your `~/.dynocap` cache. This tutorial uses Node. For other projects, prepare `python:3.14` (uv), `go:1.27` (Go modules) or `shell:1` (bash, git and ssh only), and pass the same `--runtime` to `clone`. See [Manage Dependencies and Caching](../how-to/manage-dependencies-and-caching.mdx) for each runtime's install commands. --- ## Step 2: Set Up an Example Project For this tutorial, create a small sample project with a test suite in a temporary directory: ```sh mkdir -p /tmp/demo-project cd /tmp/demo-project # Initialize package.json cat << 'EOF' > package.json { "name": "demo-project", "version": "1.0.0", "type": "module", "scripts": { "test": "node --test" } } EOF # Create a sample test cat << 'EOF' > index.test.js import test from 'node:test'; import assert from 'node:assert/strict'; test('simple math', () => { assert.equal(1 + 1, 2); }); EOF # Initialize git repository git init git add . git commit -m "Initial commit" ``` --- ## Step 3: Clone the Project into an Isolated Sandbox Now import the project into a named sandbox using `dynocap clone`: ```sh dynocap clone /tmp/demo-project agent-task --network none ``` Output: ```text cloned /tmp/demo-project into agent-task (task-8f92b7c1) phase: running runtime: node:24 network: none ttl: 30m Next: dynocap exec agent-task -- npm test ``` Let's understand what happened: - DynoCap created a disposable ext4 disk containing a copy of your Git checkout. - It booted a Linux guest with Node 24. - The working directory defaults to `/workspace`. - The network is set to `none`: the guest has no outbound Internet connectivity. --- ## Step 4: Run Commands Inside the Project Execute the test suite inside the isolated guest: ```sh dynocap exec agent-task -- npm test ``` Output: ```text ✔ simple math (1.234ms) ℹ tests 1 ℹ suites 0 ℹ pass 1 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 52.34 ``` The command ran completely inside the Linux virtual machine. Now simulate an AI agent adding a new feature and an accompanying test. Use `dynocap exec` to append code: ```sh dynocap exec agent-task -- sh -c "cat << 'EOF' > feature.js export function greet(name) { return \`Hello, \${name}!\`; } EOF" dynocap exec agent-task -- sh -c "cat << 'EOF' >> index.test.js test('greet function', async () => { const { greet } = await import('./feature.js'); assert.equal(greet('World'), 'Hello, World!'); }); EOF" ``` Re-run the test suite to verify the changes: ```sh dynocap exec agent-task -- npm test ``` Output: ```text ✔ simple math (1.102ms) ✔ greet function (0.841ms) ℹ tests 2 ℹ suites 0 ℹ pass 2 ``` --- ## Step 5: Preview the Diff Before exporting, you can preview the changes made by the sandbox at any time: ```sh dynocap diff agent-task ``` Output: ```diff diff --git a/feature.js b/feature.js new file mode 100644 index 0000000..745b78b --- /dev/null +++ b/feature.js @@ -0,0 +1,3 @@ +export function greet(name) { + return `Hello, ${name}!`; +} diff --git a/index.test.js b/index.test.js index 4b92c41..9f81a7d 100644 --- a/index.test.js +++ b/index.test.js @@ -4,3 +4,8 @@ test('simple math', () => { assert.equal(1 + 1, 2); }); + +test('greet function', async () => { + const { greet } = await import('./feature.js'); + assert.equal(greet('World'), 'Hello, World!'); +}); ``` `dynocap diff` streams clean, standard unified diff bytes to stdout without altering or halting the running sandbox. --- ## Step 6: Review Command Execution Logs DynoCap records every execution in a tamper-resistant execution journal. Discover what commands ran: ```sh dynocap logs agent-task --list ``` Output: ```text ID STATE STARTED EXIT STDOUT_BYTES STDERR_BYTES CMD 0 completed 2026-11-12 16:30:10 0 182 0 npm test 1 completed 2026-11-12 16:31:02 0 0 0 sh -c cat << 'EOF' > feature.js... 2 completed 2026-11-12 16:31:25 0 0 0 sh -c cat << 'EOF' >> index.test.js... 3 completed 2026-11-12 16:31:40 0 238 0 npm test ``` You can replay the exact stdout and stderr of any specific execution: ```sh dynocap logs agent-task --exec 3 ``` --- ## Step 7: Finish the Task and Apply the Patch When work is complete, run `dynocap finish`: ```sh dynocap finish agent-task ``` Output: ```text stopped agent-task id: task-8f92b7c1 phase: completed patch: ~/.dynocap/tasks/task-8f92b7c1/result/changes.patch result: ~/.dynocap/tasks/task-8f92b7c1/result retention: 6d 23h (expires 2026-11-19 16:30:00 UTC) Apply changes to your checkout with: git apply --check '~/.dynocap/tasks/task-8f92b7c1/result/changes.patch' && git apply '~/.dynocap/tasks/task-8f92b7c1/result/changes.patch' ``` Notice what happened: 1. Command admission closed immediately. 2. The verified patch and changed files were written to the host result directory. 3. The guest microVM and its disposable virtual disk were destroyed. 4. Your host source repository `/tmp/demo-project` remained 100% untouched until you decide to apply the patch. Now apply the verified patch to your host repo: ```sh cd /tmp/demo-project git apply ~/.dynocap/tasks/task-8f92b7c1/result/changes.patch git status ``` Output: ```text On branch main Changes not staged for commit: modified: index.test.js Untracked files: feature.js ``` --- ## Step 8: Clean Up Retained Results The retained result directory, metadata, and logs will automatically be swept after 7 days. If you want to delete them immediately: ```sh dynocap rm agent-task --purge ``` Output: ```text removed agent-task; deleted its retained record, logs, and result ``` --- ## Summary You have completed the full lifecycle of an autonomous agent task in DynoCap: - **Zero Host Risk:** Untrusted code executed inside a Linux microVM without host filesystem access or network privileges. - **Auditable Execution:** Full execution logs and verified diffs were produced. - **Explicit Integration:** You applied the changes to your working tree using standard `git apply` only after review. --- # Manage Dependencies and Caching How to warm a runtime's package cache under filtered network egress and install from it in completely offline sandboxes, for npm, uv and Go modules. HTML: https://dynocap.dev/docs/how-to/manage-dependencies-and-caching Running a package install inside an untrusted sandbox creates a dilemma. The packages have to be downloaded, but giving an agent full Internet access risks data exfiltration or credential theft. DynoCap solves this by pairing **filtered network egress** with **lockfile-keyed dependency seeds**. Each runtime has exactly one package manager. The runtime declares which registry hosts filtered egress may reach and how its package cache is keyed. This guide shows you how to: 1. Warm a dependency seed in a one-time sandbox whose egress is restricted to your runtime's registry. 2. Reuse that seed in later sandboxes that have no network device. 3. Invalidate or refresh the seed when your lockfile changes. --- ## Runtimes at a glance | Runtime | Package manager | Seed key (lockfile) | Filtered egress allows | Online install | Offline install | | ------------- | --------------- | ------------------- | ------------------------------------ | ------------------ | ---------------------------- | | `node:24` | npm | `package-lock.json` | `registry.npmjs.org` | `npm ci` | `npm ci --offline` | | `node:22` | npm | `package-lock.json` | `registry.npmjs.org` | `npm ci` | `npm ci --offline` | | `python:3.14` | uv | `uv.lock` | `pypi.org`, `files.pythonhosted.org` | `uv sync --frozen` | `uv sync --offline --frozen` | | `go:1.27` | Go modules | `go.sum` | `proxy.golang.org`, `sum.golang.org` | `go mod download` | `go mod download` | | `shell:1` | none | no seed | only a task repository URL's host | n/a | n/a | When `--runtime` is omitted, the project's files choose the runtime (`package.json` → `node:24`, `pyproject.toml` → `python:3.14`, `go.mod` → `go:1.27`); anything else gets `shell:1`. Prepare a runtime once per home before you use it, for example `dynocap runtime prepare . --runtime python:3.14`. Some files change the seed key besides the lockfile: `.npmrc` for npm and `uv.toml` for uv. The prepared runtime's digest is also part of the key, so a seed is never restored into a different runtime or version. uv uses the runtime's Python and never downloads an interpreter. Go runs with `GOTOOLCHAIN=local` and never switches to another toolchain. --- ## The workflow pattern: "deps" sandbox vs. "worker" sandbox 1. **Warm step:** Clone into a sandbox named `deps` with `--network filtered`, run the online install, and `finish`. Finishing publishes the package cache as a seed. 2. **Offline step:** Clone again with the default `--network none`. DynoCap restores the matching seed before your first command, and the offline install needs no network. ``` +-------------------------------------------------------------+ | 1. WARM SEED (deps) | | dynocap clone ./project deps --runtime python:3.14 \ | | --network filtered | | dynocap exec deps -- uv sync --frozen | | dynocap finish deps ---> Saves a seed keyed by uv.lock | +-------------------------------------------------------------+ | v +-------------------------------------------------------------+ | 2. OFFLINE TASKS (worker) | | dynocap clone ./project worker --runtime python:3.14 | | dynocap exec worker -- uv sync --offline --frozen | | (Installs from the restored seed, zero network) | +-------------------------------------------------------------+ ``` --- ## Step 1: Warm the dependency seed Make sure your project contains its runtime's lockfile, such as `package-lock.json`, `uv.lock` or `go.sum`. Then clone it with filtered networking: ```sh dynocap clone ./my-project deps --runtime node:24 --network filtered ``` > **Why `--network filtered`?** > Under filtered mode, DynoCap connects a virtio-net interface whose host forwarder answers DNS only for the runtime's registry hosts and, for a repository URL, that URL's host. It forwards only HTTPS to those names. Every other address and port is dropped. Run the online install: ```sh dynocap exec deps --timeout 300 -- npm ci ``` Finish the sandbox: ```sh dynocap finish deps ``` Finishing archives the runtime's package cache (for example `/work/npm-cache/_cacache`, `/work/uv-cache` or `/work/go/pkg/mod/cache/download`) into `~/.dynocap/dependency-cache/.tar` on the host. A seed is only published if the lockfile is unchanged at finish. It is also skipped if any command pointed the package manager at another registry or cache, for example through `npm_config_*`, `UV_INDEX*`, `GOPROXY` or `GOFLAGS`, or a `--registry` or `--index` argument. Clean up the finished record: ```sh dynocap rm deps ``` --- ## Step 2: Run worker tasks completely offline Clone your project again for your agent or test runner, leaving the network at its default (`none`): ```sh dynocap clone ./my-project worker --runtime node:24 ``` If the lockfile matches a seed, DynoCap unpacks it into the guest before `clone` returns. Run the offline install: ```sh dynocap exec worker -- npm ci --offline ``` Then run your tests or let your coding agent work: ```sh dynocap exec worker -- npm test ``` --- ## Which tasks publish a seed A seed is the package cache a networked install produced, and later offline sandboxes trust it. DynoCap therefore publishes a seed only from a task where: - every command was one of the runtime's install commands: `npm ci` or `npm install` for node, `uv sync` for python, `go mod download` for go (a plain `sh -c "npm ci"` from the SDK counts; anything with shell syntax does not); - no command pointed the package manager at another registry or cache. A task that runs anything else, such as `npm test`, a shell, or a script, never publishes or republishes a seed, because that code could rewrite the cache. Keep the `deps` sandbox to the install alone. Seeds are keyed by the task's source (its directory or repository URL) as well as the lockfile, so a seed never crosses into another project. A seed is only as trustworthy as the install that produced it: packages' install scripts ran with network access. npm re-checks each package against the lockfile's integrity hashes when it installs offline; uv and Go reuse their caches as they are. --- ## Cache invalidation and updates - **If you change dependencies:** a lockfile change produces a different key, so later clones find no matching seed. - **To update the seed:** re-run Step 1 with `--network filtered`. - **Location and size:** seeds live in `~/.dynocap/dependency-cache/`. They are capped at 2 GiB in total, and the least recently restored seeds are evicted first. You can inspect or delete them safely: ```sh ls -lh ~/.dynocap/dependency-cache ``` --- ## Troubleshooting ### A registry name does not resolve in a `--network none` sandbox The sandbox has no network device. Use the offline form of the install: `npm ci --offline`, `uv sync --offline --frozen`, or `go mod download` after a seed was restored. ### Download blocked during an install in filtered mode Filtered egress allows HTTPS only to the hosts in the table above. Dependencies from private Git repositories, a custom registry (Artifactory, GitHub Packages, a private PyPI index) or a Go `GOPROXY` other than `proxy.golang.org` are blocked. In those cases use `--network nat`, which is unrestricted. A seed is still not published when a command overrides the registry. --- # Network Isolation and Egress Policy How to configure network modes in DynoCap, enforce strict egress boundaries, and prevent data exfiltration. HTML: https://dynocap.dev/docs/how-to/network-isolation-and-egress A core security premise of DynoCap is that code generated or executed by an AI agent must not have unrestricted access to the Internet by default. This guide explains how to configure network egress policies for both raw sandboxes and project tasks. --- ## Network Modes Overview DynoCap supports three explicit network modes: | Mode | Virtual Network Device | Outbound Permitted | Use Case | | :----------------- | :------------------------ | :------------------------------------------------ | :-------------------------------------------------------- | | `none` _(default)_ | None (no `virtio-net`) | **Zero network traffic** | Running untrusted code, tests, and agent modifications. | | `filtered` | `virtio-net` + Host proxy | **HTTPS (443) only** to npm registry and Git host | Dependency warming (`npm ci`) and public Git cloning. | | `nat` | `virtio-net` + macOS NAT | Unrestricted outbound | Tasks requiring external APIs or non-standard registries. | --- ## 1. Offline Mode (`--network none`) By default, any `dynocap run` or `dynocap clone` command boots without a network device: ```sh # Explicitly or by default dynocap clone ./my-project sandbox --network none ``` Inside the Linux guest: - The kernel exposes only the loopback interface (`lo`). - There is no `eth0` or `enp0s1` virtual network adapter. - Packet transmission is physically impossible at the hypervisor level. Even if an attacker inside the VM executes `curl`, `nc`, or attempts DNS lookups, the operations fail immediately with `Network is unreachable`. --- ## 2. Filtered Egress Mode (`--network filtered`) In filtered mode, DynoCap attaches a virtual network interface backed by a dedicated host-level user-space forwarder (`FilteredForwarder.swift`): ```sh dynocap clone ./my-project sandbox --network filtered ``` ### Allowlist Rules The forwarder inspects initial TLS ClientHello packets and socket connections: 1. **Port 443 Only:** Non-HTTPS ports (e.g. HTTP on port 80, SSH on port 22, raw TCP) are dropped. 2. **Designated Hostnames:** exact names only, never their subdomains. - The task runtime's package registry, as declared in the prepared runtime manifest: - `node:24`, `node:22`: `registry.npmjs.org` - `python:3.14`: `pypi.org`, `files.pythonhosted.org` - `go:1.27`: `proxy.golang.org`, `sum.golang.org` - `shell:1`: none - The remote Git origin host if the project was cloned from an HTTPS URL. - A plain `dynocap run` sandbox has no runtime and keeps `registry.npmjs.org`. A direct connection to another IP or domain (e.g., pastebins, command-and-control servers, webhooks) is terminated at the host boundary. > **Filtered egress is not an exfiltration barrier.** The allowed hosts accept data: `npm publish` reaches `registry.npmjs.org`, `proxy.golang.org` fetches module paths on attacker-chosen domains, and `git push` reaches the repository host. Assume a filtered sandbox can send out anything it can read. See [What filtered egress does not stop](../explanation/security-and-isolation.mdx#what-filtered-egress-does-not-stop). ### Inspecting Active Network Configuration You can verify the network policy of any running sandbox: ```sh dynocap inspect sandbox --json ``` The output will clearly state: ```json { "network": "filtered", "phase": "running" } ``` --- ## 3. Unrestricted NAT Mode (`--network nat`) For tasks that legitimately require general Internet access, such as downloading datasets, interacting with external web APIs, or installing system-level packages via `apk`: ```sh dynocap clone ./my-project sandbox --network nat ``` Under NAT mode: - Apple's `VZNATNetworkDeviceAttachment` provides NAT routing through your Mac's active network interfaces. - The guest obtains an IP via DHCP from macOS. - Outbound traffic is unrestricted; inbound traffic from external networks remains unreachable. - The guest can reach your local network and services your Mac listens on through any interface, such as development servers bound to `0.0.0.0`. > **Caution:** Do not use `nat` for untrusted agent evaluations when secrets or sensitive files are in scope, as the agent can send arbitrary payloads over the Internet. --- ## Network Restrictions in TypeScript SDK & MCP When automating tasks programmatically or via LLM tool-calling (MCP): ### TypeScript SDK The SDK defaults to `none`: ```ts import { Sandbox, Task } from "@dynocap/sdk"; // Offline task const task = await Task.start({ source: "./my-project", network: "none", }); // Require offline enforcement (throws if sandbox has network) const task = await Task.open("my-task", { requireOffline: true }); ``` ### Model Context Protocol (MCP) Server To protect developers, the `@dynocap/mcp` server rejects `nat` and `filtered` commands by default. You must explicitly start the MCP server with environment variables enabling them: ```sh # Enable filtered network mode for agents DYNOCAP_MCP_ALLOW_FILTERED=1 npx @dynocap/mcp # Enable unrestricted NAT mode DYNOCAP_MCP_ALLOW_NAT=1 npx @dynocap/mcp ``` If an LLM requests a sandbox with `network: "nat"` and the flag is not set, the tool returns: ```text NAT is disabled by this MCP host; start the server with DYNOCAP_MCP_ALLOW_NAT=1 to permit it ``` --- # Interactive Shells and Terminal Sessions How to open interactive terminals, handle terminal resizing and signals, and run full-screen console programs in DynoCap. HTML: https://dynocap.dev/docs/how-to/interactive-shells-and-terminal While AI agents frequently run automated non-interactive commands, human developers often need to inspect a running sandbox, run text editors, inspect processes with `top` or `ps`, or test commands manually. This guide covers: 1. Opening an interactive shell using `dynocap shell`. 2. Running single terminal-aware commands using `dynocap exec -t`. 3. Signal handling (Ctrl-C, window resize, signals). 4. Terminal session lifecycle vs. Sandbox TTL. --- ## Interactive Shell: `dynocap shell` To open an interactive session in any running sandbox: ```sh dynocap shell my-sandbox ``` Output: ```text /workspace # whoami root /workspace # node -v v24.x.x /workspace # exit dynocap: left the shell; my-sandbox is still running until its TTL. Export with `dynocap finish my-sandbox` or discard with `dynocap rm my-sandbox --force`. ``` ### Characteristics of `dynocap shell`: - **Shell executable:** The guest automatically selects `/bin/sh` (BusyBox `ash` with tab completion and arrow key history) or the user's defined shell in `/etc/passwd`. - **Working directory:** Starts in `/workspace` for project tasks or `/` for raw sandboxes. - **Environment:** Sets `TERM=xterm-256color`, `PATH`, `HOME=/root`, and the runtime's environment variables. - **No Command Deadline:** Unlike `dynocap exec` (which defaults to a 60-second command deadline), `shell` has no command timeout. The session remains open until you exit or the sandbox reaches its overall TTL. - **Sandbox survives exit:** When you type `exit` or press `Ctrl-D`, only your interactive shell session ends. The guest virtual machine continues running in the background. --- ## Single Terminal Commands: `dynocap exec -t` When you want to run a single command that requires a pseudo-terminal (PTY), such as an interactive test runner, a progress bar, or a cursor-positioned tool, use `dynocap exec -t`: ```sh dynocap exec my-sandbox -t -- npm test -- --watch ``` ### Differences: `exec -t` vs `shell` | Property | `dynocap exec -t NAME -- COMMAND` | `dynocap shell NAME` | | :-------------------- | :-------------------------------------- | :-------------------------------------------- | | **Command** | Explicit argument (e.g. `htop`, `vi`) | Guest `/bin/sh` | | **Command Timeout** | Defaults to 60s (`--timeout` overrides) | None (bounded only by sandbox TTL) | | **Host Output** | May be piped or redirected | Must be attached to an interactive terminal | | **Post-exit message** | None | Prints reminder that sandbox is still running | --- ## What Guest Output Can Do to Your Terminal Guest output is untrusted. A terminal acts on escape sequences: they can write your clipboard (OSC 52), retitle the window, add hyperlinks, or make the terminal answer a query by typing into your shell. - `exec` without `-t` and `logs`: when stdout or stderr is a terminal, DynoCap passes text, colors, `\r`, `\b`, and the line-erase and column moves progress bars use, and drops every other escape sequence and control character. - `diff`: on a terminal nothing is interpreted. Control characters appear as `^[`, `^M` and so on, and characters that hide or reorder text (bidi overrides, zero-width characters) appear as ``, so hidden content in a patch is visible. - `shell` and `exec -t` are a real terminal session, like `ssh`, and are not filtered. Use them only with sandboxes whose output you are willing to let your terminal interpret. Output to a pipe or file is never filtered. To see the exact bytes, pipe through a viewer: `dynocap diff demo | less -R` or `dynocap logs demo | cat -v`. --- ## Signal Handling and Keystrokes When attached to a terminal session in DynoCap: ### 1. Interrupting a Process (Ctrl-C) - When running `dynocap exec` without `-t`, pressing `Ctrl-C` forwards `SIGINT` over vsock to the guest process group. If the guest exits from the signal, the CLI exits with status `130`. - In a terminal session (`shell` or `exec -t`), `Ctrl-C` is passed as a standard ASCII byte (`0x03`) to the guest PTY driver, triggering standard terminal interrupt behavior. ### 2. Window Resizing (SIGWINCH) When you resize your macOS terminal window (e.g. Terminal.app, iTerm2, Alacritty, Ghostty), the DynoCap CLI intercepts `SIGWINCH` on the host, reads the new row and column dimensions, and transmits a control packet over vsock. The guest agent issues an `ioctl(TIOCSWINSZ)` on the guest PTY, so full-screen tools (such as `nano`, `less`, or `htop`) match the new size. ### 3. Terminating from the Host If the host CLI receives `SIGTERM`, `SIGHUP`, or `SIGQUIT`, it cleanly restores your host terminal's original termios state before exiting (`128 + signal`), ensuring your macOS shell does not remain in raw mode or echo-disabled. --- ## Standard Input Modes for `dynocap exec` For automated scripting, `dynocap exec` supports several stdin modes: ```sh # 1. Pipe a local file or script into the guest cat script.sh | dynocap exec my-sandbox -- sh # 2. Interactive non-PTY streaming (read host stdin until EOF) dynocap exec my-sandbox -i -- python3 # 3. Explicitly disable stdin (immediate EOF to guest) dynocap exec my-sandbox --no-input -- my-command ``` | Flag | Host Stdin Behavior | Guest Stdin Behavior | | :-------------------- | :----------------------- | :------------------------------------- | | _(Default, pipe)_ | Read from host until EOF | Streams bytes to guest, then sends EOF | | _(Default, terminal)_ | Not read | Sends immediate EOF to guest | | `-i`, `--interactive` | Read from host until EOF | Keeps stdin open for interactive input | | `--no-input` | Not read | Immediate EOF | --- # Integrate the TypeScript SDK How to programmatically start sandboxes, execute commands, preview diffs, and collect results using @dynocap/sdk. HTML: https://dynocap.dev/docs/how-to/integrate-typescript-sdk DynoCap provides a native TypeScript/JavaScript SDK (`@dynocap/sdk`) for developers building AI agent harnesses, automated evaluators, or developer tool integrations. The SDK communicates directly with the local user daemon over UNIX domain socket `~/.dynocap/daemon.sock` (mode `0600`). --- ## Installation Within the `dynocap` repository or a local workspace linking the package: ```json { "dependencies": { "@dynocap/sdk": "file:./sdk/typescript" } } ``` ```sh npm install ``` --- ## Pattern 1: Managing Project Tasks (`Task` API) The `Task` class provides the full project lifecycle: cloning a workspace, executing commands, streaming output, previewing diffs, and exporting results. ```ts import { Task, SandboxError } from "@dynocap/sdk"; async function runAgent() { try { // 1. Start a task with an imported project directory const task = await Task.start({ name: "pr-fixer", projectPath: "./my-project", runtimes: [{ name: "node", version: "24" }], network: "none", // offline by default ttlSeconds: 1800, // 30-minute sandbox lifetime }); console.log(`Spawned task ${task.id} (name: ${task.name})`); // 2. Execute a command in /workspace const result = await task.exec("npm test", { timeoutSeconds: 60, }); console.log(`Exit code: ${result.exitCode}`); console.log(`Stdout: ${result.stdout}`); // 3. Inspect a real-time diff preview const preview = await task.diff(); console.log(`Verified patch path: ${preview.patchPath}`); // 4. Finish the task: exports result & destroys the VM const finished = await task.finish(); console.log(`Final patch exported to: ${finished.patchPath}`); console.log(`Result directory: ${finished.resultDirectory}`); } catch (error) { if (error instanceof SandboxError) { console.error(`DynoCap error [${error.kind}]: ${error.message}`); } else { throw error; } } } runAgent(); ``` --- ## Pattern 2: Raw Ephemeral Sandboxes (`Sandbox` API) If you only need an isolated minimal Linux execution environment without importing a project workspace or generating diffs, use `Sandbox`: ```ts import { Sandbox } from "@dynocap/sdk"; async function executeInSandbox() { // Boot raw sandbox const sandbox = await Sandbox.create({ name: "eval-box", cpus: 2, memoryMB: 1024, network: "none", ttlSeconds: 300, }); // Run command const execResult = await sandbox.exec("uname -a && cat /etc/alpine-release", { timeoutSeconds: 10, }); console.log(execResult.stdout); // Clean up await sandbox.destroy(); } ``` --- ## Re-attaching to Existing Tasks Tasks can be re-opened by their friendly name or immutable ID across different processes: ```ts import { Task } from "@dynocap/sdk"; // Open existing task by name const task = await Task.open("pr-fixer", { requireOffline: true, // throw if sandbox has network access enabled }); // Run further commands await task.exec("npm run build"); ``` --- ## Streaming Standard Input & Terminal Transcripts The SDK allows piping standard input bytes and capturing combined terminal transcripts: ```ts // Pipe stdin bytes to a guest process const inputBytes = new TextEncoder().encode("console.log(process.version);\n"); const res = await task.exec("node", { stdin: inputBytes, }); // Run in a pseudo-terminal (PTY) const ttyRes = await task.exec("npm test", { terminal: { rows: 24, cols: 80 }, }); // ttyRes.stdout contains combined terminal transcript (including ANSI controls) console.log(ttyRes.stdout); ``` --- ## Listing and Housekeeping Query managed sandboxes and purge expired tasks: ```ts import { listManaged, removeManaged } from "@dynocap/sdk"; // List all active and retained tasks const entries = await listManaged(); for (const entry of entries) { console.log( `${entry.name ?? entry.id}: phase=${entry.phase}, TTL=${entry.sandboxExpiresAtUnix}` ); } // Purge a completed task and remove retained logs/results await removeManaged("pr-fixer", { purge: true }); ``` --- # Configure the Model Context Protocol (MCP) Server How to connect DynoCap's MCP server to Claude Desktop, Cursor, and Cline to empower LLMs with safe, local sandboxes. HTML: https://dynocap.dev/docs/how-to/configure-mcp-server The **Model Context Protocol (MCP)** enables AI coding assistants like Claude Desktop, Cursor, and Cline to call local tools. DynoCap includes a first-party MCP server package (`@dynocap/mcp`) that exposes tools for starting tasks, executing commands in an isolated Linux microVM, inspecting diffs, and exporting patches. --- ## Prerequisites 1. DynoCap CLI installed (`brew install baires/tap/dynocap`, the curl installer, or `./scripts/build-macos.sh` from a checkout). 2. The runtime your projects use, prepared with `dynocap doctor --fix ./project` (or `--runtime python:3.14`, for example). When a task omits `runtimes`, DynoCap picks one from the project's files, the same way `clone` does, and falls back to `shell:1` (bash, git, ssh). The task result reports the runtime used. 3. Build the TypeScript packages: ```sh npm run typecheck npm run --prefix sdk/mcp build ``` --- ## MCP Server Security Policies By default, `@dynocap/mcp` enforces strict security boundaries: - **Offline Only by Default:** The LLM cannot allocate network interfaces or connect to external hosts. - **Opt-in Network Modes:** You must explicitly enable network flags via environment variables when launching the server: - `DYNOCAP_MCP_ALLOW_FILTERED=1`: Permits HTTPS access to the task runtime's package registry (`registry.npmjs.org` for node, `pypi.org` and `files.pythonhosted.org` for python, `proxy.golang.org` and `sum.golang.org` for go, and none for `shell:1`) and, for a repository URL, that URL's host. - `DYNOCAP_MCP_ALLOW_NAT=1`: Permits unrestricted outbound NAT connectivity. - **Server-chosen host paths:** `projectPath` must be inside `DYNOCAP_MCP_ROOTS` (default: the server's working directory) and is never the home directory or a dot directory in it. Desktop clients often start servers from `/` or `~`, which leaves no default root, so set `DYNOCAP_MCP_ROOTS` to your project directories. - **Repository hosts and keys:** `repositoryUrl` must name a host in `DYNOCAP_MCP_GIT_HOSTS` (default `github.com,gitlab.com,bitbucket.org`), and `sshKeyPath` needs `DYNOCAP_MCP_ALLOW_SSH_KEY=1`. Filtered egress limits destinations, not what leaves: the registries and Git hosts it allows accept uploads. See [What filtered egress does not stop](../explanation/security-and-isolation.mdx#what-filtered-egress-does-not-stop). --- ## Connecting to Claude Desktop Add DynoCap to your Claude Desktop configuration file: 📁 `~/Library/Application Support/Claude/claude_desktop_config.json`: ```json { "mcpServers": { "dynocap": { "command": "node", "args": ["/Users/YOUR_USER/playground/dynocap/sdk/mcp/dist/bin.js"], "env": { "DYNOCAP_MCP_ALLOW_FILTERED": "1", "DYNOCAP_MCP_ROOTS": "/Users/YOUR_USER/code" } } } } ``` Restart Claude Desktop. You will see the DynoCap tools appear in the hammer icon list. --- ## Connecting to Cursor To configure Cursor to use DynoCap: 1. Open **Cursor Settings** -> **Features** -> **MCP Servers**. 2. Click **+ Add New MCP Server**. 3. Set: - **Name:** `dynocap` - **Type:** `command` - **Command:** `node /Users/YOUR_USER/playground/dynocap/sdk/mcp/dist/bin.js` --- ## Connecting to Cline / Roo-Code In Cline/Roo-Code MCP settings: ```json { "mcpServers": { "dynocap": { "command": "node", "args": ["/Users/YOUR_USER/playground/dynocap/sdk/mcp/dist/bin.js"], "env": { "DYNOCAP_MCP_ALLOW_FILTERED": "1" } } } } ``` --- ## Available MCP Tools Once connected, your AI assistant can use the following tools: | MCP Tool | Purpose | | :-------------------- | :---------------------------------------------------------------------- | | `dynocap_task_start` | Imports a project tree or Git URL into an isolated task sandbox. | | `dynocap_task_exec` | Runs a shell command inside the task's `/workspace`. Streams output. | | `dynocap_diff` | Previews unified Git diffs of modifications made by the agent. | | `dynocap_task_finish` | Shuts down the sandbox and saves the verified patch and execution logs. | | `dynocap_ls` | Lists active sandboxes and retained task records. | | `dynocap_rm` | Destroys sandboxes or purges retained task directories. | | `dynocap_run` | Boots a raw Linux sandbox without a project tree. | | `dynocap_exec` | Executes a command inside a raw sandbox. | | `dynocap_destroy` | Destroys a raw sandbox. | --- ## Typical Prompt Example You can prompt your assistant like this: > _"Clone `./my-web-app` into a DynoCap sandbox named `feature-auth`. Run `npm test` inside the sandbox to make sure it's green. Then implement OAuth token validation in `src/auth.js`, verify the tests pass, show me the diff preview, and finish the task."_ The assistant will autonomously invoke `dynocap_task_start`, `dynocap_task_exec`, `dynocap_diff`, and `dynocap_task_finish` safely within the Linux microVM. --- # Troubleshoot with dynocap doctor How to use dynocap doctor to find what is blocking sandboxes, and dynocap doctor --fix to repair it. HTML: https://dynocap.dev/docs/how-to/troubleshoot-with-doctor `dynocap doctor` checks everything a sandbox needs and tells you, for each problem, what is wrong and what to run. `dynocap doctor --fix` repairs the problems it can. ```sh dynocap doctor # report only; changes nothing dynocap doctor --fix # repair, then report again dynocap doctor --fix ./project # check and prepare the runtime ./project uses ``` The runtime doctor checks is the one your project uses: `package.json` means `node:24`; `pyproject.toml`, `uv.lock`, `requirements.txt`, `setup.py`, or `Pipfile` means `python:3.14`; `go.mod` means `go:1.27`. `clone` picks the same way. Pass `--runtime NAME:VERSION` to choose another. With no project (for example, in the dynocap checkout), doctor requires no runtime and lists the prepared ones. `--fix` builds from the dynocap checkout, which it finds from the `dynocap` binary, so you can run it in any directory. --- ## Reading the Report ```text Home: /Users/you/.dynocap OK platform macOS 13+; guest architecture aarch64 OK signing CLI signature validates and includes the virtualization entitlement. ERROR images The guest image for aarch64 does not match its manifest. Next: Run dynocap doctor --fix to build the guest image (downloads pinned Alpine files; needs Go). ERROR daemon The running daemon started before this dynocap binary was rebuilt, so it runs the old build. Next: Run dynocap doctor --fix to restart the daemon with this dynocap build. 2 required checks failed. Run dynocap doctor --fix to repair them. ``` - `OK` passed; a stopped daemon or a home that does not exist yet is normal. `WARNING` is worth knowing but does not block sandboxes, such as having no runtime prepared outside a project. `ERROR` blocks sandboxes, and the command exits 1. - `Next:` is the one thing to do. When it names `dynocap doctor --fix`, the command already includes any `--home` or `--runtime` you passed, so you can paste it as printed. - The last line says whether `--fix` can repair everything, some of it, or nothing. For scripts, `dynocap doctor --json` prints the same report. Each check has a `fix` field naming its automatic repair. See [JSON Schemas](../reference/cli/json-schemas.mdx). --- ## What `--fix` Repairs | Check | Repair | Needs | | :-------- | :------------------------------------------------------------------------------------------------- | :---------------------------------- | | `daemon` | Stops a broken daemon, or one from an older build, and starts one from this binary. | No running sandboxes | | `images` | Builds the guest kernel and initrd. Mismatched files are moved to `replaced-