feat: versioned Node runtimes — node22/npm22/npx22, generalized cb expose (RM-11) - #30
Merged
Merged
Conversation
… cb expose (RM-11) - Add built-in `node22`, `npm22`, `npx22` profiles using `node:22-slim` and the `node22` state group, keeping the same logical volume names as the existing `node24` family so state isolation is provided by `state_group`. - Generalize `cb expose` to accept any npm-shaped stateful profile already in the registry (e.g. `npm` or `npm22`); exposed tools inherit the source profile's image, state group and env/volume configuration. - Update tests: registry count (16), node22 profile assertions, volume-name identity, `AppendMissingDefaultTools` pre-RM-11 upgrade, `Expose` guard clauses, and lockfile distinct `node:22-slim` / `node:24-slim` entries. - Update README, docs/security-model.md and docs/architecture.md. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ing) GLM-5.2's round-1 verify flagged that cb expose terraform (a registry tool that exists but has no npm-global shared volume) was the one daemon-free guard clause left untested, and that the new %q-formatted error could in principle render as tool "" if Tool.Name weren't populated. Pins both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds Node 22 as an isolated compatibility runtime and generalizes npm binary exposure.
Changes:
- Adds
node22,npm22, andnpx22profiles. - Preserves source runtime settings for exposed npm binaries.
- Updates tests and runtime documentation.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents Node 22 and generalized exposure. |
| main.go | Updates CLI help text. |
| internal/registry/state_group_test.go | Tests runtime state isolation. |
| internal/registry/registry.go | Defines Node 22 profiles. |
| internal/registry/registry_test.go | Tests profile configuration. |
| internal/registry/file_test.go | Tests registry upgrades. |
| internal/lockfile/lockfile_test.go | Tests distinct image locking. |
| internal/cli/cli.go | Generalizes npm exposure. |
| internal/cli/cli_test.go | Tests exposure guard paths. |
| docs/security-model.md | Updates exposure guidance. |
| docs/architecture.md | Documents separate lock entries. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…-11) - discoverNPMGlobalBins now resolves the discovery image through lockfile.RuntimeImageForTool, matching every other execution path. - Expose rejects non-stateful source tools before any Docker run. - Shared-volume matching is by container destination (/cb/npm-global) rather than hardcoded logical name. - Existing-tool skip message now includes the existing tool's state_group. - Extracted renderExposedToolSection and added TestRenderExposedToolSection to prove exposed sections inherit the source tool's Node identity. - README notes that node22/npm22/npx22 are added on upgrade but must be locked before use. - Added a deferred-scoping comment for node22 self-test steps in diag.go. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (1)
README.md:204
- The README says to run
cb lock/cb update --all“before using” the newnode22profiles, but a few lines later it documents that “no lockfile → … UNLOCKED mode”. To avoid implyingnode22cannot be used without a lockfile, make this sentence conditional on already having a lockfile (i.e., upgrading a previously-locked install).
fully isolating project `node_modules`, the npm cache and the npm global prefix; upgrading an existing installation adds these profiles automatically, but they are not yet locked, so run `cb lock` or `cb update --all` before using them.
… (RM-11 Devin finding) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
58 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A real MCP integration (
@whdrnr2583/token-metervianpx) surfaced a gap: one npm package'snative addon (
better-sqlite3@^11.5.0) has no compatible prebuild undernode:24-slim(nocompiler toolchain in the slim image), while the same package works fine under
node:22-slim.ContainerBin only shipped one Node runtime, so there was no way to select a different major per
tool without hand-editing the registry and re-deriving all the isolation semantics yourself.
Filed as issue #28 / RM-11 in the roadmap (issue #2).
What changed
The registry (
internal/registry) already had everything needed for this to be aconfiguration-only change:
internal/pathmap's volume-ID functions already namespace every stateful volume bystate_group("cb-" + group + "-" + name[+hash]), so two profiles with differentstate_groupvalues get fully isolated state — projectnode_modules, npm cache, npmglobal prefix — even when they reuse identical logical volume names.
lockfile.ConfiguredImagesalready collects distinctt.Imagestrings generically, so asecond Node image gets its own lock entry for free.
AppendMissingDefaultToolsalready diffs against the built-in defaults and appends missingsections non-destructively, so existing installs pick up the new profiles automatically on
the next
cb setup/cb install.So this PR is:
node22/npm22/npx22onnode:22-slim,state_group = "node22". The Node 24 profiles (node/npm/npx) are byte-for-byteunchanged and remain the default.
cb exposegeneralized: the one hardcoded!= "npm"literal check is now a registrytool-name lookup, so
cb expose npm22 <binary>becomes possible with no new CLI flag —the existing positional argument already carries the meaning once the hardcode is removed.
The generated
[tools.NAME]section now correctly inherits the source tool's image,state group, shared volumes and env settings — so a binary exposed from
npm22keeps itsNode 22 identity instead of silently falling back to the default Node 24 state group.
README.md,docs/architecture.md,docs/security-model.md) updated in the same PR.Deliberately only two Node lines (24 default, 22 compatibility/LTS alternative) — no
Node 20/21/23, no
cb add-runtimecommand, no implicit/automatic runtime fallback. Runtimeselection stays explicit and inspectable, matching the issue's own non-goals.
internal/pathmap,internal/lockfileresolution, andinternal/dockerrunare untouched —verified during scoping that isolation and locking already work generically; this PR is
registry/CLI configuration only.
Pipeline
Implemented by SWE-1.7 Max, verified (blind, pasted-context) by GLM-5.2 High. Round 1 verify
found one real gap: the non-npm-shaped-source-tool guard clause in
cb expose(e.g.
cb expose terraform) was the one daemon-free code path left untested, and the new%q-formatted error could in principle have rendered astool ""ifTool.Nameweren'tpopulated from the parser. Fixed directly by the orchestrator with a test
(
TestExposeRejectsNonNpmShapedTool) —Tool.Nameis in fact populated correctly, this onlyneeded test coverage, not a code change.
Independently re-validated (not just the implementer's report) inside
golang:1.24:gofmtsilent,vetclean, all packages pass including the three new/extended test files.Validation
🤖 Generated with a multi-agent pipeline (SWE-1.7 Max implementer, GLM-5.2 High blind verifier, orchestrated by Claude Code)