nandi/oripublic Fork 0
f5c963af3c1597c0274ce4a99705dbd92a7d1cba
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

🎉 Begin a project. 4edda86 · on f5c963af3c1597c0274ce4a99705dbd92a7d1cba · k33g · yesterday
README.md · 268 lines · 16.0 KBmarkdown
Blame HistoryOpen raw

dev-toolkit kit

A Docker Sandboxes v2 mixin kit that provisions a general-purpose development toolkit:
a code-quality engine, pinned Go / Rust / Node toolchains, and three Claude Code skills
covering code quality, documentation, and a controlled feature-development workflow.

Tool Version Installed at Notes
qlty 0.639.0 /usr/local/bin/qlty Lint + smells + metrics engine, for any language.
Go 1.26.5 /usr/local/go Symlinked into /usr/local/bin, shadowing the template's own /usr/bin/go.
TinyGo 0.41.1 /usr/local/tinygo TINYGOROOT set; for wasm, wasip1, wasip2 targets.
Rust 1.97.1 /usr/local/rustup, /usr/local/cargo Via rustup, minimal profile + clippy + rustfmt, with the wasm32-wasip2 target.
Node.js 24.19.0 /usr/local/node LTS "Krypton". Shadows the template's own /usr/bin/node (22.x).
pnpm 11.22.0 /usr/local/share/npm-global/bin Plus bundled corepack, for projects that pin a version via packageManager.
vsce / ovsx 3.9.2 / 1.1.1 /usr/local/share/npm-global/bin Package and publish VS Code extensions to the Marketplace / Open VSX.
gcc, g++ Ubuntu's /usr/bin The base template has no C compiler; Rust and node-gyp both need one.
Skill Purpose
quality Configures qlty, measures the project, writes a report, tracks progression, refactors until the gate passes.
diataxis-doc Writes or restructures documentation with the Diátaxis four-quadrant method.
methodical-dev Orchestrator. Drives a feature through requirements → plan → implementation → quality gate → bilingual docs → project memory, stopping for approval at every step. Invokes the other two.

Plus a ~/.claude/CLAUDE.md, loaded at the start of every session, which brackets the work at
both ends: read the project's committed .memory/ record — summary, history, latest
handoff — before planning or asking the user anything, and update all three before handing
control back, unconditionally, however small the session. summary.md is edited in place and
never regenerated from one session's partial view; history.md gets exactly one appended
entry; handoffs/ gets today's file. methodical-dev (Phase 7) holds the full templates —
CLAUDE.md is what makes the same discipline apply to ad-hoc work that never invokes the skill.

Only the toolchain binaries are language-specific. qlty is not a Go tool: qlty init
detects the languages present and enables the matching linters, and all three skills are
language-agnostic. Drop the toolchain install steps from spec.yaml and the kit is a pure
quality/docs/workflow mixin for any stack. The install steps are independent of each other,
so a Go-only or Rust-only sandbox is a matter of deleting the ones you do not want — except
the C-toolchain step, which Rust and native npm addons both depend on.

Project-specific skills do not belong here. builtin-parity, which describes the
GoloScript repository in particular, lives in the sibling golo-toolkit
kit and is composed in only where it applies.

Layout

kits/dev-toolkit/
├── spec.yaml                              # schemaVersion "2", kind: mixin
├── README.md
└── files/home/.claude/
    ├── CLAUDE.md                          # loaded every session: read .memory/ first
    └── skills/
        ├── quality/
        │   ├── SKILL.md                   # the workflow the agent follows
        │   └── scripts/quality_report.py  # measurement, report, history, gate verdict
        ├── diataxis-doc/SKILL.md
        └── methodical-dev/SKILL.md

files/home/** is copied into the agent's home at container start, so the skills land
under ~/.claude/skills/ and Claude Code picks them up as personal skills, and CLAUDE.md
lands at ~/.claude/CLAUDE.md — the user-level memory file, prepended to every Claude
Code session in the sandbox regardless of which project is open.

Why both a CLAUDE.md and a skill say to read .memory/. methodical-dev's Phase 0
only runs when that skill is invoked. ~/.claude/CLAUDE.md is loaded unconditionally, so an
ad-hoc request ("fix this bug") also starts by reading the project record instead of
re-deriving state that is already written down. Keep the file short — it costs tokens on
every single session; the detail belongs in the skills and in the project's own
.memory/README.md.

Everything in that tree is packed into the kit and shipped into the sandbox, so keep it to
the skills themselves — a stray __pycache__/ would be shipped alongside them. The
repository .gitignore guards against that.

Usage

# create the sandbox with the kit — `--kit` at create time, not `sbx kit add`,
# so the cache volumes are attached (see Known limitations)
sbx run claude --kit ./kits/dev-toolkit

Then, in the agent session: "run a quality report", "document this project with
Diátaxis"
, or "let's build this feature methodically".

Editor extensions

The two ecosystems use different toolchains, and both are provisioned:

VS Code Zed
Language TypeScript / JavaScript on Node Rust compiled to WebAssembly
Manifest package.json (contributes, engines.vscode) extension.toml + Cargo.toml
Build the project's own script (tsc, esbuild) cargo build --release --target wasm32-wasip2
Package vsce package → a .vsix Zed's own extension builder, run by the editor
Publish vsce publish, ovsx publish a PR to zed-industries/extensions

wasm32-wasip2 is preinstalled because that is the target Zed compiles extensions with;
building for anything else produces an artifact it refuses. Both flows were verified inside
the sandbox end to end — npm installtscvsce package.vsix, and
zed_extension_apicargo build --target wasm32-wasip2 → a WASM component (layer
0100, not a core module).

What does not work here, by construction: no editor runs in the sandbox, so VS Code's F5
Extension Development Host and Zed's Install Dev Extension are host-side actions, and
@vscode/test-electron needs an X server this kit does not install. Compile, unit-test, lint
and package in the sandbox; drive the editor on the host.

Design notes

Why a mixin and not a kind: sandbox kit. The kit adds tooling to whatever agent you
are already using rather than defining its own image, so it composes: --kit it onto
claude, codex, shell, or anything else. Exactly one sandbox kit is allowed per
composition; mixins stack freely.

Why pinned tarballs instead of the official installers. curl https://qlty.sh | sh
resolves latest (so two sandboxes built a day apart differ), POSTs an install event to
cdp.customer.io, and rewrites the user's shell rc files. Every artifact here is pinned
by version and SHA256, verified with sha256sum -c before it is unpacked — the same
pattern the official trivy, vale, and mise kits use. qlty's own published
.sha256 files match the digests in spec.yaml.

Why Python unpacks the qlty archive. qlty ships only .tar.xz and the sandbox
template has no xz binary. Installing xz-utils would mean running apt-get update,
which re-fetches every configured apt source — including the template's
download.docker.com — and fails if any one of them is not allow-listed. python3 is
present, and lzma is in its standard library, so the archive is decompressed with no
extra egress at all.

Toolchain pairing is load-bearing. TinyGo 0.41.1 accepts Go 1.19–1.26 and refuses a
GOROOT newer than the Go it was itself built with (1.26.2). Go 1.26.5 satisfies both
bounds. Bumping Go to 1.27 breaks TinyGo — bump the two together. GOTOOLCHAIN=local
prevents Go from silently downloading a different toolchain behind your back.

Why a C compiler is a hard requirement, not a nicety. The base template ships make
but no cc, gcc, clang, ld or libc headers. That produces a genuinely misleading
failure: cargo build --target wasm32-wasip2 succeeds without a C compiler, because
rustc links wasm targets with its own bundled rust-lld — while cargo test, which builds
for the host, dies with error: linker `cc` not found. A Zed extension therefore appears
to build fine and cannot be unit-tested, which is the worst possible shape for a missing
dependency. The same gap breaks any crate with a C-compiling build.rs, and any npm package
with a native addon built through node-gyp. apt-get update is deliberately not run: the
template ships populated /var/lib/apt/lists, so a plain apt-get install works, whereas
update re-fetches every configured source (Docker's included) and fails if one is not
allow-listed. It stays only as a fallback for the day those lists go stale.

Why rustup rather than the standalone Rust tarballs. A pinned rustup-init binary
(digest-verified, not the curl https://sh.rustup.rs | sh bootstrapper, which resolves
latest and edits shell rc files) buys a working rustup target add — which matters because
Zed itself shells out to exactly that when an extension needs a target that is not installed.
Rust lives under /usr/local rather than in the home directory, for the same reason Go does:
the install runs once at container creation, and only paths baked into the image survive a
restart. Both RUSTUP_HOME and CARGO_HOME are left world-writable — the layout the
official rust Docker image uses — so rustup target add, rustup component add and
cargo install work as the agent user without sudo.

Why the kit does not set npm's prefix. The obvious move — npm_config_prefix=/usr/local,
so global installs land in /usr/local/bin — is wrong here. The base template already exports
NPM_CONFIG_PREFIX=/usr/local/share/npm-global, and that directory is agent-owned and
already on PATH ahead of /usr/local/bin
. Overriding it would move global installs into a
root-owned tree and break npm install -g for the agent user, which currently works with no
sudo. The install step passes --prefix explicitly (rather than trusting the variable to be
exported into a root-run step) and then chowns the tree back to the agent.

--allow-scripts is not optional for vsce. npm 11 skips unrecognised lifecycle scripts
and merely warns. Two of vsce's dependencies need theirs: keytar builds the native addon
that stores a publishing PAT, and @vscode/vsce-sign unpacks the signing binary. Skipped,
they fail at vsce publish time — long after the install that caused it.

The cache volumes. Three, all of them things that are otherwise re-downloaded on a cold
start: ~/.qlty (qlty provisions a JVM for radarlint-go, Node, and each linter binary on
demand), /usr/local/cargo/registry (Cargo's .crate files and sparse index) and ~/.npm.
The Cargo one is deliberately the registry subdirectory rather than all of CARGO_HOME:
mounting an empty volume over CARGO_HOME would shadow the rustup shims in its bin. Volumes
are fixed at container creation, so sbx kit add warns and skips them — use --kit at create
time to get the caches.

Maintenance

Bumping a version. Change the version and both per-arch SHA256 values in the
matching setup.install entry in spec.yaml. Sources:

  • Go — https://go.dev/dl/?mode=json (files[].sha256)
  • TinyGo — compute from the release tarball; the project publishes no checksums file
  • qlty — the <asset>.tar.xz.sha256 file next to each release asset
  • Node — https://nodejs.org/dist/v<version>/SHASUMS256.txt. Pick from the active LTS
    line; https://nodejs.org/dist/index.json marks it with a non-false lts field. Note the
    .tar.gz digest, not .tar.xz: the template has no xz, and unlike qlty, Node publishes
    both — which is why this step needs no python-lzma workaround
  • rustup — https://static.rust-lang.org/rustup/archive/<ver>/<triple>/rustup-init.sha256,
    with the current version at https://static.rust-lang.org/rustup/release-stable.toml
  • Rust itself — no digest to update; rustup resolves and verifies the toolchain. The current
    stable version is the [pkg.rust] version in
    https://static.rust-lang.org/dist/channel-rust-stable.toml
  • vsce / ovsx / pnpm — npm view <pkg> version

Before bumping the Zed target. wasm32-wasip2 is not a preference — it is read from Zed's
own extension builder (RUST_TARGET in crates/extension/src/extension_builder.rs). Confirm
there before changing it; an extension built for another target is one Zed refuses to load.

Validating a change.

sbx kit validate ./kits/dev-toolkit
sbx kit inspect ./kits/dev-toolkit --json | jq '.warnings'   # expect null or []

Empty warnings is the green light — a non-empty list means a v1 surface crept in that
will stop loading at the spec's Phase 6 cutover.

Install commands completed only means the commands exited 0. Verify the outcome:

sbx exec <sandbox> -- sh -lc '
  go version && tinygo version && qlty --version &&
  rustc --version && cargo --version && rustup target list --installed &&
  node --version && npm --version && pnpm --version && vsce --version && ovsx --version &&
  cc --version | head -1'

And verify the thing versions cannot tell you — that host-target Rust actually links,
which is what the C-toolchain step exists for:

sbx exec <sandbox> -- sh -lc '
  cd "$(mktemp -d)" && cargo init --lib -q . && cargo test 2>&1 | tail -3'

Extending the network allowlist. permissions.network.allow covers the install downloads,
qlty's plugin/linter resolution, the Go module proxy, Cargo's three hosts (the sparse index
index.crates.io, the CDN static.crates.io, and the API crates.io — all three are needed),
Ubuntu's archives for the C toolchain, and the VS Code Marketplace / Open VSX publishing paths.
A linter for another language will need its registry added. Find out what is missing rather
than guessing:

sbx policy log <sandbox>

That prints what the proxy actually allowed and blocked. Add blocked hosts one at a time.

Known limitations

  • sbx kit add does not write mixin context. The engine gates the kit-memory write on
    the artifact's own AI-profile filename, which mixins deliberately do not carry (agentInstructions.filename is ignored for a mixin), so adding this
    kit at runtime silently skips kits-memory/dev-toolkit.md. Create the sandbox with
    --kit instead. The same applies to the ~/.qlty volume.
  • The skills and CLAUDE.md are Claude Code-specific. The toolchain is not. The kit sets
    no requires.agent, so it works with any agent; on non-Claude agents those files are inert
    and agentInstructions carries the same instructions — the .memory/-first rule and the
    quality script invocation.
  • Only linux/amd64 and linux/arm64 are supported. Other architectures fail the
    install with an explicit error rather than installing something wrong.
  • No editor, and no display. Nothing in this kit can run VS Code or Zed. The
    Extension Development Host (F5), Zed's Install Dev Extension, and any
    @vscode/test-electron run all need a GUI session on the user's machine; there is no X
    server here and the kit does not install xvfb. The sandbox covers compile, unit-test, lint
    and package — write extensions so their logic is testable without the vscode module and
    the split costs you nothing.
  • Tree-sitter grammars in Zed extensions are not reproducible here. Compiling a parser
    needs the wasi-sdk, which Zed downloads itself, on the host. The Rust half of such an
    extension builds fine; the grammar half does not happen in this sandbox.
  • The C toolchain is the one unpinned install. Go, TinyGo, qlty, Node and rustup are all
    version-and-digest pinned; gcc comes from Ubuntu's archives at whatever version they serve
    (15.2.0 at time of writing). Pinning it would mean pinning a .deb set and its transitive
    closure, which is a much larger commitment than it is worth for a linker driver — but it does
    mean two sandboxes built months apart can differ here.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# `dev-toolkit` kit

A Docker Sandboxes **v2 mixin kit** that provisions a general-purpose development toolkit:
a code-quality engine, pinned Go / Rust / Node toolchains, and three Claude Code skills
covering code quality, documentation, and a controlled feature-development workflow.

| Tool | Version | Installed at | Notes |
| --- | --- | --- | --- |
| qlty | 0.639.0 | `/usr/local/bin/qlty` | Lint + smells + metrics engine, for any language. |
| Go | 1.26.5 | `/usr/local/go` | Symlinked into `/usr/local/bin`, shadowing the template's own `/usr/bin/go`. |
| TinyGo | 0.41.1 | `/usr/local/tinygo` | `TINYGOROOT` set; for `wasm`, `wasip1`, `wasip2` targets. |
| Rust | 1.97.1 | `/usr/local/rustup`, `/usr/local/cargo` | Via rustup, `minimal` profile + `clippy` + `rustfmt`, with the `wasm32-wasip2` target. |
| Node.js | 24.19.0 | `/usr/local/node` | LTS "Krypton". Shadows the template's own `/usr/bin/node` (22.x). |
| pnpm | 11.22.0 | `/usr/local/share/npm-global/bin` | Plus bundled `corepack`, for projects that pin a version via `packageManager`. |
| vsce / ovsx | 3.9.2 / 1.1.1 | `/usr/local/share/npm-global/bin` | Package and publish VS Code extensions to the Marketplace / Open VSX. |
| gcc, g++ | Ubuntu's | `/usr/bin` | The base template has **no** C compiler; Rust and node-gyp both need one. |

| Skill | Purpose |
| --- | --- |
| `quality` | Configures qlty, measures the project, writes a report, tracks progression, refactors until the gate passes. |
| `diataxis-doc` | Writes or restructures documentation with the Diátaxis four-quadrant method. |
| `methodical-dev` | Orchestrator. Drives a feature through requirements → plan → implementation → quality gate → bilingual docs → project memory, stopping for approval at every step. Invokes the other two. |


Plus a `~/.claude/CLAUDE.md`, loaded at the start of every session, which brackets the work at
both ends: **read** the project's committed `.memory/` record — summary, history, latest
handoff — before planning or asking the user anything, and **update all three** before handing
control back, unconditionally, however small the session. `summary.md` is edited in place and
never regenerated from one session's partial view; `history.md` gets exactly one appended
entry; `handoffs/` gets today's file. `methodical-dev` (Phase 7) holds the full templates —
`CLAUDE.md` is what makes the same discipline apply to ad-hoc work that never invokes the skill.

**Only the toolchain binaries are language-specific.** qlty is not a Go tool: `qlty init`
detects the languages present and enables the matching linters, and all three skills are
language-agnostic. Drop the toolchain install steps from `spec.yaml` and the kit is a pure
quality/docs/workflow mixin for any stack. The install steps are independent of each other,
so a Go-only or Rust-only sandbox is a matter of deleting the ones you do not want — except
the C-toolchain step, which Rust and native npm addons both depend on.

Project-specific skills do **not** belong here. `builtin-parity`, which describes the
GoloScript repository in particular, lives in the sibling [`golo-toolkit`](../golo-toolkit/)
kit and is composed in only where it applies.

## Layout

```
kits/dev-toolkit/
├── spec.yaml                              # schemaVersion "2", kind: mixin
├── README.md
└── files/home/.claude/
    ├── CLAUDE.md                          # loaded every session: read .memory/ first
    └── skills/
        ├── quality/
        │   ├── SKILL.md                   # the workflow the agent follows
        │   └── scripts/quality_report.py  # measurement, report, history, gate verdict
        ├── diataxis-doc/SKILL.md
        └── methodical-dev/SKILL.md
```

`files/home/**` is copied into the agent's home at container start, so the skills land
under `~/.claude/skills/` and Claude Code picks them up as personal skills, and `CLAUDE.md`
lands at `~/.claude/CLAUDE.md` — the user-level memory file, prepended to **every** Claude
Code session in the sandbox regardless of which project is open.

**Why both a `CLAUDE.md` and a skill say to read `.memory/`.** `methodical-dev`'s Phase 0
only runs when that skill is invoked. `~/.claude/CLAUDE.md` is loaded unconditionally, so an
ad-hoc request ("fix this bug") also starts by reading the project record instead of
re-deriving state that is already written down. Keep the file short — it costs tokens on
every single session; the detail belongs in the skills and in the project's own
`.memory/README.md`.

Everything in that tree is packed into the kit and shipped into the sandbox, so keep it to
the skills themselves — a stray `__pycache__/` would be shipped alongside them. The
repository `.gitignore` guards against that.

## Usage

```bash
# create the sandbox with the kit — `--kit` at create time, not `sbx kit add`,
# so the cache volumes are attached (see Known limitations)
sbx run claude --kit ./kits/dev-toolkit
```

Then, in the agent session: *"run a quality report"*, *"document this project with
Diátaxis"*, or *"let's build this feature methodically"*.

### Editor extensions

The two ecosystems use different toolchains, and both are provisioned:

| | VS Code | Zed |
| --- | --- | --- |
| Language | TypeScript / JavaScript on Node | Rust compiled to WebAssembly |
| Manifest | `package.json` (`contributes`, `engines.vscode`) | `extension.toml` + `Cargo.toml` |
| Build | the project's own script (tsc, esbuild) | `cargo build --release --target wasm32-wasip2` |
| Package | `vsce package` → a `.vsix` | Zed's own extension builder, run by the editor |
| Publish | `vsce publish`, `ovsx publish` | a PR to `zed-industries/extensions` |

`wasm32-wasip2` is preinstalled because that is the target Zed compiles extensions with;
building for anything else produces an artifact it refuses. Both flows were verified inside
the sandbox end to end — `npm install``tsc``vsce package``.vsix`, and
`zed_extension_api``cargo build --target wasm32-wasip2` → a WASM **component** (layer
`0100`, not a core module).

What does **not** work here, by construction: no editor runs in the sandbox, so VS Code's F5
Extension Development Host and Zed's *Install Dev Extension* are host-side actions, and
`@vscode/test-electron` needs an X server this kit does not install. Compile, unit-test, lint
and package in the sandbox; drive the editor on the host.

## Design notes

**Why a mixin and not a `kind: sandbox` kit.** The kit adds tooling to whatever agent you
are already using rather than defining its own image, so it composes: `--kit` it onto
`claude`, `codex`, `shell`, or anything else. Exactly one `sandbox` kit is allowed per
composition; mixins stack freely.

**Why pinned tarballs instead of the official installers.** `curl https://qlty.sh | sh`
resolves `latest` (so two sandboxes built a day apart differ), POSTs an install event to
`cdp.customer.io`, and rewrites the user's shell rc files. Every artifact here is pinned
by version **and** SHA256, verified with `sha256sum -c` before it is unpacked — the same
pattern the official `trivy`, `vale`, and `mise` kits use. qlty's own published
`.sha256` files match the digests in `spec.yaml`.

**Why Python unpacks the qlty archive.** qlty ships only `.tar.xz` and the sandbox
template has no `xz` binary. Installing `xz-utils` would mean running `apt-get update`,
which re-fetches *every* configured apt source — including the template's
`download.docker.com` — and fails if any one of them is not allow-listed. `python3` is
present, and `lzma` is in its standard library, so the archive is decompressed with no
extra egress at all.

**Toolchain pairing is load-bearing.** TinyGo 0.41.1 accepts Go 1.19–1.26 *and* refuses a
GOROOT newer than the Go it was itself built with (1.26.2). Go 1.26.5 satisfies both
bounds. Bumping Go to 1.27 breaks TinyGo — bump the two together. `GOTOOLCHAIN=local`
prevents Go from silently downloading a different toolchain behind your back.

**Why a C compiler is a hard requirement, not a nicety.** The base template ships `make`
but no `cc`, `gcc`, `clang`, `ld` or libc headers. That produces a genuinely misleading
failure: `cargo build --target wasm32-wasip2` **succeeds** without a C compiler, because
rustc links wasm targets with its own bundled `rust-lld` — while `cargo test`, which builds
for the host, dies with ``error: linker `cc` not found``. A Zed extension therefore appears
to build fine and cannot be unit-tested, which is the worst possible shape for a missing
dependency. The same gap breaks any crate with a C-compiling `build.rs`, and any npm package
with a native addon built through node-gyp. `apt-get update` is deliberately *not* run: the
template ships populated `/var/lib/apt/lists`, so a plain `apt-get install` works, whereas
`update` re-fetches every configured source (Docker's included) and fails if one is not
allow-listed. It stays only as a fallback for the day those lists go stale.

**Why rustup rather than the standalone Rust tarballs.** A pinned `rustup-init` *binary*
(digest-verified, not the `curl https://sh.rustup.rs | sh` bootstrapper, which resolves
`latest` and edits shell rc files) buys a working `rustup target add` — which matters because
Zed itself shells out to exactly that when an extension needs a target that is not installed.
Rust lives under `/usr/local` rather than in the home directory, for the same reason Go does:
the install runs once at container creation, and only paths baked into the image survive a
restart. Both `RUSTUP_HOME` and `CARGO_HOME` are left world-writable — the layout the
official `rust` Docker image uses — so `rustup target add`, `rustup component add` and
`cargo install` work as the agent user without sudo.

**Why the kit does *not* set npm's prefix.** The obvious move — `npm_config_prefix=/usr/local`,
so global installs land in `/usr/local/bin` — is wrong here. The base template already exports
`NPM_CONFIG_PREFIX=/usr/local/share/npm-global`, and that directory is **agent-owned and
already on PATH ahead of `/usr/local/bin`**. Overriding it would move global installs into a
root-owned tree and break `npm install -g` for the agent user, which currently works with no
sudo. The install step passes `--prefix` explicitly (rather than trusting the variable to be
exported into a root-run step) and then `chown`s the tree back to the agent.

**`--allow-scripts` is not optional for vsce.** npm 11 skips unrecognised lifecycle scripts
and merely warns. Two of vsce's dependencies need theirs: `keytar` builds the native addon
that stores a publishing PAT, and `@vscode/vsce-sign` unpacks the signing binary. Skipped,
they fail at `vsce publish` time — long after the install that caused it.

**The cache volumes.** Three, all of them things that are otherwise re-downloaded on a cold
start: `~/.qlty` (qlty provisions a JVM for `radarlint-go`, Node, and each linter binary on
demand), `/usr/local/cargo/registry` (Cargo's `.crate` files and sparse index) and `~/.npm`.
The Cargo one is deliberately the `registry` subdirectory rather than all of `CARGO_HOME`:
mounting an empty volume over `CARGO_HOME` would shadow the rustup shims in its `bin`. Volumes
are fixed at container creation, so `sbx kit add` warns and skips them — use `--kit` at create
time to get the caches.

## Maintenance

**Bumping a version.** Change the version *and* both per-arch `SHA256` values in the
matching `setup.install` entry in `spec.yaml`. Sources:

- Go — <https://go.dev/dl/?mode=json> (`files[].sha256`)
- TinyGo — compute from the release tarball; the project publishes no checksums file
- qlty — the `<asset>.tar.xz.sha256` file next to each release asset
- Node — `https://nodejs.org/dist/v<version>/SHASUMS256.txt`. Pick from the **active LTS**
  line; `https://nodejs.org/dist/index.json` marks it with a non-`false` `lts` field. Note the
  `.tar.gz` digest, not `.tar.xz`: the template has no `xz`, and unlike qlty, Node publishes
  both — which is why this step needs no python-lzma workaround
- rustup — `https://static.rust-lang.org/rustup/archive/<ver>/<triple>/rustup-init.sha256`,
  with the current version at `https://static.rust-lang.org/rustup/release-stable.toml`
- Rust itself — no digest to update; rustup resolves and verifies the toolchain. The current
  stable version is the `[pkg.rust] version` in
  `https://static.rust-lang.org/dist/channel-rust-stable.toml`
- vsce / ovsx / pnpm — `npm view <pkg> version`

**Before bumping the Zed target.** `wasm32-wasip2` is not a preference — it is read from Zed's
own extension builder (`RUST_TARGET` in `crates/extension/src/extension_builder.rs`). Confirm
there before changing it; an extension built for another target is one Zed refuses to load.

**Validating a change.**

```bash
sbx kit validate ./kits/dev-toolkit
sbx kit inspect ./kits/dev-toolkit --json | jq '.warnings'   # expect null or []
```

Empty warnings is the green light — a non-empty list means a v1 surface crept in that
will stop loading at the spec's Phase 6 cutover.

`Install commands completed` only means the commands exited `0`. Verify the outcome:

```bash
sbx exec <sandbox> -- sh -lc '
  go version && tinygo version && qlty --version &&
  rustc --version && cargo --version && rustup target list --installed &&
  node --version && npm --version && pnpm --version && vsce --version && ovsx --version &&
  cc --version | head -1'
```

And verify the thing versions cannot tell you — that host-target Rust actually **links**,
which is what the C-toolchain step exists for:

```bash
sbx exec <sandbox> -- sh -lc '
  cd "$(mktemp -d)" && cargo init --lib -q . && cargo test 2>&1 | tail -3'
```

**Extending the network allowlist.** `permissions.network.allow` covers the install downloads,
qlty's plugin/linter resolution, the Go module proxy, Cargo's three hosts (the sparse index
`index.crates.io`, the CDN `static.crates.io`, and the API `crates.io` — all three are needed),
Ubuntu's archives for the C toolchain, and the VS Code Marketplace / Open VSX publishing paths.
A linter for another language will need its registry added. Find out what is missing rather
than guessing:

```bash
sbx policy log <sandbox>
```

That prints what the proxy actually allowed and blocked. Add blocked hosts one at a time.

## Known limitations

- **`sbx kit add` does not write mixin context.** The engine gates the kit-memory write on
  the artifact's own AI-profile filename, which mixins deliberately do not carry (`agentInstructions.filename` is ignored for a mixin), so adding this
  kit at runtime silently skips `kits-memory/dev-toolkit.md`. Create the sandbox with
  `--kit` instead. The same applies to the `~/.qlty` volume.
- **The skills and `CLAUDE.md` are Claude Code-specific.** The toolchain is not. The kit sets
  no `requires.agent`, so it works with any agent; on non-Claude agents those files are inert
  and `agentInstructions` carries the same instructions — the `.memory/`-first rule and the
  `quality` script invocation.
- **Only `linux/amd64` and `linux/arm64`** are supported. Other architectures fail the
  install with an explicit error rather than installing something wrong.
- **No editor, and no display.** Nothing in this kit can run VS Code or Zed. The
  Extension Development Host (F5), Zed's *Install Dev Extension*, and any
  `@vscode/test-electron` run all need a GUI session on the user's machine; there is no X
  server here and the kit does not install `xvfb`. The sandbox covers compile, unit-test, lint
  and package — write extensions so their logic is testable without the `vscode` module and
  the split costs you nothing.
- **Tree-sitter grammars in Zed extensions are not reproducible here.** Compiling a parser
  needs the wasi-sdk, which Zed downloads itself, on the host. The Rust half of such an
  extension builds fine; the grammar half does not happen in this sandbox.
- **The C toolchain is the one unpinned install.** Go, TinyGo, qlty, Node and rustup are all
  version-and-digest pinned; `gcc` comes from Ubuntu's archives at whatever version they serve
  (15.2.0 at time of writing). Pinning it would mean pinning a `.deb` set and its transitive
  closure, which is a much larger commitment than it is worth for a linker driver — but it does
  mean two sandboxes built months apart can differ here.