Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,17 @@ secret cache: DPAPI blobs in `~/.sbx`, shared with the generated CLI — FR-051)
executed by the extension), `sandbox.ts` (recipe→refs + naming + lifecycle), `ops.ts`
(per-sandbox create/attach/rebuild/shell shared by palette + Explorer; owns the progress
spinners, the one-operation-per-sandbox guard FR-054 and cancellation FR-056),
`git.ts` (read-only host git probes: `isShallowRepository` for the clone-mount preflight,
FR-058 — never mutates a repo),
`log.ts` (operation log FR-055: the `Sandbox Console` channel + the spawn runner every
sbx/docker call streams through — process plumbing only, no CLI strings), `names.ts`
(per-working-copy `workspaceState` record of sbx names that can no longer be created,
FR-057), `terminal.ts`
(native terminals driving sbx), `form.ts` (webview Configure form), `tree.ts` (Sandbox
(native terminals driving sbx + the one host terminal, `openHostCommandTerminal`, that
types the FR-058 fix without running it), `form.ts` (webview Configure form), `tree.ts` (Sandbox
Explorer view + per-node commands), `agents.ts`/`services.ts` (registries + discovery).
Dependency direction: `extension → {ops, form, tree, sandbox, config, identity, agents, names, script, secrets, sbx, log}`;
`ops → {images, secrets, sandbox, terminal, names, sbx, log}`;
`ops → {images, secrets, sandbox, terminal, names, git, sbx, log}`; `git → log`;
`tree → {ops, form, sandbox, config, identity, agents, sbx, log}`;
`form → {ops, secrets, sandbox, config, identity, agents, names, script, sbx}`;
`secrets → {blobs, sandbox, services, sbx}`; `script → config`;
Expand Down
41 changes: 36 additions & 5 deletions docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ All source is in `src/`. The extension bundles to `dist/extension.js` via esbuil
| `secrets.ts` | Provisions missing service secrets — cached-entry picker / prompt → `sbx secret set` over stdin (FR-032 + FR-051, §8) — and the `Manage Cached Secrets` command. |
| `blobs.ts` | The per-project secret cache store (FR-051, §8): `~/.sbx/<entry>.<service>.dpapi` blobs, encrypted/decrypted via a PowerShell child process (DPAPI; value over stdin/stdout pipes only). Shared on disk with the generated CLI. |
| `script.ts` | Renders and maintains the generated project CLI `.sandbox/scripts/sbx.sh` (FR-052, §13): version+hash header, silent refresh of unmodified copies, never overwrites manual edits silently. |
| `ops.ts` | Per-sandbox create/attach/stop/rebuild/destroy/shell, shared by the palette and the Explorer so the two never drift. Owns the progress spinners, the single-flight guard (FR-054) and cancellation at stage boundaries (FR-056) — §12. |
| `ops.ts` | Per-sandbox create/attach/stop/rebuild/destroy/shell, shared by the palette and the Explorer so the two never drift. Owns the progress spinners, the single-flight guard (FR-054), cancellation at stage boundaries (FR-056) — §12 — and the mount preflight (FR-058, §5) whose modal refusal it shows itself, raising the shared `HandledError` so no surface reports it twice. |
| `git.ts` | Read-only host git probes (FR-058): `isShallowRepository` (`git rev-parse --is-shallow-repository`, resolved with `-C` so a workspace inside a repo works). Own module for the same reason `sbx.ts` is one — one place per external CLI's argv. Never mutates a repository. |
| `log.ts` | The operation log (FR-055): the `Sandbox Console` output channel plus the `spawn`-based runner every `sbx`/`docker` call goes through — streams child output to the channel and to the progress notification, and kills the child on cancel. Process plumbing only: it knows no CLI strings. |
| `names.ts` | The per-working-copy record of sbx names that can no longer be created (FR-057, §14): `workspaceState`-backed, written when a create fails with the leaked-state error, read by key derivation. Local by construction — it never reaches the committed recipe. |
| `terminal.ts` | Native VS Code terminals whose `shellPath` is `sbx` — assembles the interactive `run`/`exec` shellArgs and pools agent terminals per sandbox (§10, §12). This is where the agent actually attaches. |
| `terminal.ts` | Native VS Code terminals whose `shellPath` is `sbx` — assembles the interactive `run`/`exec` shellArgs and pools agent terminals per sandbox (§10, §12). This is where the agent actually attaches. Also opens the one **host** terminal the extension needs (`openHostCommandTerminal`): the FR-058 hand-off, which types `git fetch --unshallow` and leaves the Enter to the user. |
| `form.ts` | The New/Edit webview (§7, §12): persists to the recipe AND applies to the instance. |
| `tree.ts` | Sandbox Explorer view + per-node commands (§12). |
| `agents.ts` / `services.ts` | Static agent/secret-service registries (labels + fallback) backing the live discovery in `sbx.ts`. |
Expand All @@ -86,10 +87,10 @@ Dependency direction (verified against imports):
`extension → {ops, form, tree, sandbox, config, identity, agents, names, script, secrets, sbx, log}`;
`tree → {ops, form, sandbox, config, identity, agents, sbx, log}`;
`form → {ops, secrets, sandbox, config, identity, agents, names, script, sbx}`;
`ops → {images, secrets, sandbox, terminal, names, sbx, log}`;
`ops → {images, secrets, sandbox, terminal, names, git, sbx, log}`;
`terminal → {sandbox, agents, sbx}`; `secrets → {blobs, sandbox, services, sbx}`;
`sandbox → {config, identity, sbx, log}`; `images → {config, sbx, log}`;
`sbx → log`; `script → config`; `identity → config`.
`sbx → log`; `git → log`; `script → config`; `identity → config`.
Nothing depends on `extension`; `config`, `agents`, `services`, `blobs`, `names`, and
`log` are leaves.

Expand Down Expand Up @@ -119,6 +120,16 @@ The UI labels the attach action **Connect** (the underlying sbx operation is sti
attach via `sbx run`). Create-vs-attach is disambiguated by checking `sbx ls --json`
first, then choosing the create form (`… <agent> <path>`) or the attach form (`… <name>`).

**Preflight before the first sbx call.** Every create path (Connect, Shell, Rebuild)
first checks that the workspace can serve the sandbox's mount mode: `hostToSandboxPath`
rejects UNC/`\\wsl$` paths (FR-040), and `mount: clone` additionally requires a
non-shallow repository (FR-058, §9). Rebuild runs both **before** its removal stage, so a
refusal leaves the existing sandbox intact rather than deleting it and then declining to
recreate it. The shallow refusal is a modal dialog raised in `ops.ts` (one place for all
three surfaces) offering **Open Terminal** — `git fetch --unshallow` typed into a host
terminal, never executed — and then throws `ops.HandledError`, which every surface's
error reporter skips so the dialog is not followed by a redundant toast.

There is **no implicit default sandbox**, and startup is **always quiet and read-only** —
opening a workspace never raises notifications and **never writes into `.sandbox/`**
(spec 009). Discovery feeds the status bar (state icon + display name; `+ New Sandbox`
Expand Down Expand Up @@ -340,7 +351,11 @@ Provided by `sbx`, surfaced (not reimplemented) by the extension:
- **Network policy** (Features §12) — host proxy enforces an allow-list; outbound hosts
are logged/allowed/blocked.
- **Filesystem policy** (Features §12) — *direct* mount (read-write workspace) vs
`--clone` (private in-container clone, host repo mounted read-only).
`--clone` (private in-container clone, host repo mounted read-only at
`/run/sandbox/source`). The clone is made by sbx at every start with
`git clone --reference <source> <source> <mirrored host path>` plus a `git daemon`
serving it back as the host remote `sandbox-<name>`; `--reference` is why the mode
needs a **non-shallow** repository, checked before the create (FR-058, §5, §14).
- **Workspace mount path** — with direct mount on Windows, each host drive is mounted in
the sandbox at `/<drive-letter>` (e.g. `D:\Repositories\app` → `/d/Repositories/app`),
read-write and bidirectional. `sbx run` drops the agent there; `Shell` reaches it via
Expand Down Expand Up @@ -534,6 +549,10 @@ re-encoding these rules as prose and calls subcommands instead
same logic runs with `createIfMissing: false` — an existing script is still refreshed on
upgrade, but a missing one is **not** created merely by opening a project (spec 009;
FR-002 read-only discovery). A sibling `.gitattributes` pins LF.
- **Same preconditions as the UI.** The create path refuses `mount: clone` on a shallow
repository, with the same message and the same fail-open behaviour as `ops.ts`
(FR-058) — parity here is what keeps a script-driven create from producing the empty
workspace the UI now prevents.
- **Runners.** `runner-create <slug>` instantiates the recipe's `default: true` entry as
an **ephemeral** clone-mode instance `<name>-<key>-<id>-p<slug>` (agent/image/secret
names/caps from the recipe; defaults `-m 8g --cpus 4`) — never written back into the
Expand Down Expand Up @@ -584,5 +603,17 @@ re-encoding these rules as prose and calls subcommands instead
asked whether a name is claimed — `sbx ls` does not list it and `sbx rm` reports
"not found". The record is local (`workspaceState`), stores names only, and becomes
irrelevant after a `sbx reset`.
- **A clone-mode create can still fail silently for reasons the host cannot see.** The
in-sandbox clone runs on every start, guarded by `[ ! -d "$TARGET/.git" ]`, and the
script pre-creates `$TARGET` before cloning into it. Anything that makes that one
`git clone --reference` fail therefore leaves an **empty workspace directory** the agent
is then dropped into, with the error only in sbx's start-up output (the agent terminal),
since `sbx create` itself exited 0. It never self-heals: as soon as the agent writes into
that directory, later starts fail on
`fatal: destination path … already exists and is not an empty directory` and the original
cause is lost. Shallow sources are the reachable case and are refused up front (FR-058);
the general case would need a post-create probe (`sbx exec … test -d <dest>/.git`), which
costs a started sandbox per create and reports only after the fact — deliberately not
built. Recovery for an already-wedged sandbox is **Rebuild** once the cause is removed.
- **Deferred scope**: filesystem/network policy UIs (Features §12), MCP endpoints
(Features §13), kit injection (§7).
32 changes: 32 additions & 0 deletions docs/Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ After creation:
* Agent launches automatically
* Terminal opens automatically

A create is refused up front when the workspace cannot serve the sandbox's mount mode —
network/WSL paths (FR-040) and a shallow repository under `mount: clone` (FR-058).

---

## FR-004 Sandbox Start
Expand Down Expand Up @@ -510,6 +513,33 @@ Examples:

---

## FR-058 Clone Mount Preflight

A sandbox shall never be created into a workspace its mount mode cannot serve.

* **`mount: clone` requires an unshallowed repository.** sbx copies the repo into the
sandbox with `git clone --reference <read-only host mount>`, and git refuses a shallow
source (`--depth` clone or fetch). The create itself still succeeds, so the failure is
invisible from the extension: it happens inside the sandbox at start-up and leaves the
agent in an **empty workspace**.
* Creating such a sandbox is therefore **refused before the first sbx call** — on Connect,
Shell and Rebuild alike — in a **modal dialog** that names the cause, the fix
(`git fetch --unshallow`, and what running it does to the working copy) and the
alternative (`mount: direct`, which clones nothing). Modal because it ends the action the
user just asked for, and the explanation does not fit a notification's one line.
* The extension does not run the fix: fetching the missing history changes what the user's
repository contains, so it is theirs to run (the same rule that keeps discovery
non-mutating, FR-002). **Open Terminal** opens a host terminal in the repository with the
command typed in and *not* executed — the Enter stays with the user.
* The check fails open — no git on the host, or a workspace outside any repository, does
not block a create. sbx rejects a non-repository workspace itself, before creating
anything; shallowness is the one precondition it does not check.
* `mount: direct` is unaffected — nothing is cloned. An existing sandbox already created
this way is not repaired automatically: unshallow, then **Rebuild**.
* The generated project CLI (FR-052) refuses the same case in its create path.

---

# 10. Sandbox Explorer

A dedicated VS Code sidebar shall be available, **scoped to the current repo**.
Expand Down Expand Up @@ -616,6 +646,8 @@ subcommands instead of re-encoding naming/lifecycle/secret rules as prose
passive open never adds it (FR-002).
* The extension **never executes** the generated script — generation is one-way
(a committed script is repo-controlled input).
* Preconditions mirror the UI's: the create path refuses `mount: clone` on a shallow
repository with the same message and the same fail-open rule (FR-058).

## FR-053 Fresh Image Rebuild

Expand Down
Loading