Skip to content

feat(embed): let hosts wrap the Node runtime, not just select one - #1086

Merged
jdx merged 5 commits into
mainfrom
claude/github-discussion-1083-860e25
Jul 24, 2026
Merged

jdx merged 5 commits into
mainfrom
claude/github-discussion-1083-860e25

Conversation

@jdx

@jdx jdx commented Jul 23, 2026 •

Copy link
Copy Markdown
Collaborator

Implements the proposal in discussion #1083: extend the embedder Node hook from #1079 so a host can describe a Node invocation rather than a bin dir — covering wrappers (instrumenting runtimes, transpiling loaders, sandboxes), not just version managers.

What changed

EmbedderRuntime builder — two named constructors so no invalid combination is representable:

  • selector(bin_dir) — the version-manager case; reproduces feat(embed): let hosts supply the node runtime for lifecycle scripts #1079's node_bin_dir exactly.
  • wrapper(node_program).real_node().path_dir().version().env_append()/.env_set() — the shim on PATH and at NODE (so $NODE child.js stays wrapped), the real binary at npm_node_execpath (so node-gyp finds Node's install prefix).

The three spawn signals — bin_dir (PATH), node_program (NODE + spawn program), node_execpath (npm_node_execpath) — are now distinct in RuntimeContext.

Lifecycle-script env contribution — env_append / env_set. NODE_OPTIONS appends over the user's nodeOptions by default (a wrapper's --import preload adds rather than clobbers); env_set replaces. Applied to every spawn: jailed lifecycle scripts (folded through merge_node_options) and dlx/exec/node (inherit-and-append).

Process-wide registration — set_embedder_runtime (first-write-wins, mirroring set_embedder) covers every spawn path. A per-call InstallOptions.runtime fully replaces it. Honored regardless of runtime_switching — an explicit invocation is an override, not a resolution.

Lazy version — supplied on the runtime, not probed at seed time; engines::probe_node_version probes node_execpath (memoized) only when a version is actually needed.

Every spawn path — exec/run are decoupled from process cwd (they take an explicit base_dir, so concurrent embed calls in different projects don't race), and node gains a supervised-spawn variant (the CLI path image-replaces the process). New entry points embed::{run, exec, dlx, node} drive the run-class commands in process, each rooted at an explicit project_dir.

Every field is optional; unset reproduces standalone aube's behavior byte-for-byte, so selector is the degenerate case ({ bin_dir }).

Tests

  • runtime: selector/wrapper derivation, NODE_OPTIONS append fold, scoped seed + set-once, no-op outside scope.
  • aube-scripts: NODE vs npm_node_execpath stamped distinctly for a wrapper; execpath falls back to program for a selector; extra_env applied.
  • Full cargo test -p aube --lib (670) + -p aube-scripts (76) pass; cargo clippy --all-targets and cargo fmt --check clean.

Docs: new "Node runtime" and "Run scripts, binaries, and Node" sections in docs/embedding/rust.md.

🤖 Generated with Claude Code


Note

Medium Risk
Touches every Node spawn path (scripts, install lifecycle, pnpmfile, scanners) and changes the embed install API from a bin dir to a richer runtime model; incorrect shim/execpath wiring could break native builds or concurrent multi-project embed calls.

Overview
Replaces the embedder node_bin_dir hook with EmbedderRuntime: selector(bin_dir) for toolchain pickers (same behavior as before) and wrapper(shim) with optional real_node, internal_node, path_dir, version, and env_append / env_set so NODE can stay on a shim while npm_node_execpath points at the real binary node-gyp expects.

set_embedder_runtime registers that description process-wide; per-install InstallOptions.runtime and per-call overrides on new embed::{run, exec, dlx, node} win via with_embedder_runtime and a PER_CALL_RUNTIME task-local so nested installs (auto-install, dlx scratch) still see the host’s wrapper.

Lifecycle and non-jailed spawns get split NODE / npm_node_execpath, folded NODE_OPTIONS, and extra_env; aube’s own Node-driven paths use internal_node_program() to skip wrapper hops. run / exec / dlx / auto-install anchor on an explicit base_dir / project_dir instead of process cwd; aube node gains a supervised run_spawn path that applies the same child env as scripts.

Engines version probing is keyed off node_execpath with a per-binary cache. C/Node-API bindings still pass runtime: None (ambient node). Docs add embedding sections for runtime modes and in-process run-class commands.

Reviewed by Cursor Bugbot for commit e3c6cd2. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Expanded configurable Node.js runtime support for embedded hosts, including runtime-selection vs wrapper mode and per-install overrides.
    • Added/updated embedding APIs to run scripts, exec binaries, dlx packages, and supervise a Node subprocess against a specified project directory.
  • Bug Fixes

    • Improved Node version detection and environment stamping for wrapper/shims (correct NODE vs npm_node_execpath, NODE_OPTIONS merging, and embedder extra_env ordering).
    • Anchored auto-installs and command root resolution to the intended directory.
  • Documentation

    • Updated embedding Rust docs with new Node runtime behavior and embedded execution guidance.
  • Tests

    • Added coverage for NODE/npm_node_execpath stamping and fallbacks.

Extends the embedder Node hook (#1079) so a host can describe a Node
*invocation* rather than a bin dir, covering wrappers — instrumenting
runtimes, transpiling loaders, sandboxes — not just version managers.

- EmbedderRuntime builder with selector()/wrapper() constructors. The
  three spawn signals are now distinct: bin_dir (PATH), node_program
  (NODE + spawn program), node_execpath (npm_node_execpath, the real
  binary node-gyp reads). selector() reproduces the old node_bin_dir.
- Lifecycle-script env contribution via env_append/env_set; NODE_OPTIONS
  appends over the user's nodeOptions by default (env_set replaces).
- set_embedder_runtime process-global so every spawn path is covered;
  per-call InstallOptions.runtime replaces it. Honored regardless of
  runtime_switching.
- Version supplied, not probed; engines probes node_execpath lazily.
- exec/run decoupled from process cwd (explicit base_dir) and node gains
  a supervised-spawn variant, so embed::{run,exec,dlx,node} drive the
  run-class commands in process, each rooted at project_dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 23, 2026 20:28
@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces Node binary-directory overrides with EmbedderRuntime, separates wrapper and real Node paths, propagates embedder environment settings, adds embedded execution helpers, and supports explicit base directories for command execution.

Changes

Embedder Runtime Integration

Layer / File(s) Summary
Runtime model and environment propagation
crates/aube/src/runtime.rs, crates/aube/src/engines.rs, crates/aube/src/commands/{doctor,runtime}.rs
Runtime contexts distinguish node_program from node_execpath, support environment merge modes, and probe the configured executable.
Install and lifecycle-script plumbing
crates/aube-scripts/src/lib.rs, crates/aube/src/commands/{add,ci,deploy,install,node,script_settings}.rs, crates/aube-ffi/src/lib.rs, crates/aube-node/src/lib.rs, crates/aube/src/commands/{fetch,settings,security_scanner,pnpmfile}.rs
Install callers use EmbedderRuntime; lifecycle scripts stamp separate Node paths and environment overrides, and internal Node processes use the internal program.
Embedded command execution
crates/aube/src/embed.rs, crates/aube/src/commands/{auto_install,dlx,exec,node,run,mod}.rs
Host-facing execution helpers resolve runtimes and installations from explicit project directories, supervise Node children, and preserve CLI defaults.
Embedding API documentation
docs/embedding/rust.md
Documentation describes selector and wrapper runtimes and the embedded run-class APIs.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Host
  participant EmbedAPI
  participant Runtime
  participant CommandRunner
  participant NodeProcess
  Host->>EmbedAPI: configure runtime and invoke command
  EmbedAPI->>Runtime: resolve project runtime
  Runtime->>CommandRunner: provide Node paths and environment
  CommandRunner->>NodeProcess: spawn with base_dir, PATH, and env
  NodeProcess-->>Host: return exit code
Loading

Possibly related PRs

  • jdx/aube#1079: Updates embedded install and lifecycle-script Node runtime plumbing that this PR replaces with EmbedderRuntime.

Suggested reviewers: copilot

Poem

A rabbit tuned Node by moonlit light,
Split wrapper and real path just right.
Env vars hopped through every gate,
Scripts and commands now coordinate.
“Run,” said the hare, “with roots held tight!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: embed hosts can now wrap Node execution, not only select a binary.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Extends embedded Node execution with wrapper-aware runtime configuration and explicit project roots.

  • Separates wrapper, real Node executable, PATH, version, and child environment handling.
  • Adds embedded run, exec, dlx, and supervised Node entry points.
  • Anchors run and exec auto-install behavior to the caller-supplied project directory.
  • Updates runtime registration precedence and propagation through nested install scopes.

Confidence Score: 5/5

The PR appears safe to merge with no remaining blocking failures related to the previous review threads.

The prior project-root auto-install and late runtime-registration failures are addressed, and the existing dlx cwd concern is already covered by the displayed review feedback.

Important Files Changed

Filename Overview
crates/aube/src/runtime.rs Adds wrapper-aware runtime state, per-call propagation, and precedence that resolves the previously reported late-registration split.
crates/aube/src/commands/exec.rs Routes project discovery and auto-install through the explicit embedded base directory, addressing the prior cwd mismatch.
crates/aube/src/commands/run.rs Adds explicit base-directory propagation for embedded script execution and auto-install.
crates/aube/src/commands/dlx.rs Adds explicit project rooting for runtime resolution and the local-binary fast path.
crates/aube/src/embed.rs Exposes embedded run-class APIs with per-call runtime selection and explicit project directories.
crates/aube-scripts/src/lib.rs Separates NODE from npm_node_execpath and applies embedder-contributed environment to jailed scripts.

Reviews (5): Last reviewed commit: "fix(embed): seed the isolation scope in ..." | Re-trigger Greptile

Comment thread crates/aube/src/engines.rs Outdated
Comment thread crates/aube/src/commands/exec.rs Outdated
Comment thread crates/aube/src/embed.rs Outdated
Comment thread crates/aube/src/runtime.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends aube’s embed API so hosts can describe a full Node invocation (including wrappers/shims and env contributions), and threads that runtime through all Node spawn paths (install lifecycle scripts, run/exec/dlx/node) while avoiding process-cwd races for in-process embedding entry points.

Changes:

  • Introduces EmbedderRuntime with selector(...) and wrapper(...).real_node(...).env_append/env_set(...), and plumbs node_program vs node_execpath through RuntimeContext.
  • Adds process-wide runtime registration (set_embedder_runtime) and per-install override seeding, plus lazy version probing via node_execpath.
  • Adds in-process embed helpers embed::{run, exec, dlx, node} and updates command implementations to accept an explicit base directory to avoid cwd contention.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
docs/embedding/rust.md Documents the new embedder Node runtime model and new in-process run/exec/dlx/node APIs.
crates/aube/src/runtime.rs Implements EmbedderRuntime, runtime seeding, env merge logic, and node_program/node_execpath split.
crates/aube/src/engines.rs Probes Node version via the active runtime’s node_execpath when available.
crates/aube/src/embed.rs Exposes embed-facing runtime types and adds in-process run/exec/dlx/node entry points.
crates/aube/src/commands/script_settings.rs Folds embedder NODE_OPTIONS into resolved nodeOptions and passes execpath/extra env into script settings.
crates/aube/src/commands/runtime.rs Updates runtime reporting to use node_program naming.
crates/aube/src/commands/run.rs Adds run_script_in / base-dir plumbing to avoid process-cwd races for embedding.
crates/aube/src/commands/node.rs Applies child env vars to aube node and adds supervised spawn variant for in-process embedding.
crates/aube/src/commands/install/mod.rs Seeds embedder runtime into the install scope before runtime resolution.
crates/aube/src/commands/install/args.rs Renames embedder install option from embedder_node_bin_dir to embedder_runtime.
crates/aube/src/commands/exec.rs Adds run_in with explicit base dir and enables Default for args construction.
crates/aube/src/commands/doctor.rs Updates doctor output to use node_program naming.
crates/aube/src/commands/dlx.rs Adds run_in with explicit base dir and enables Default for args construction.
crates/aube/src/commands/deploy/mod.rs Updates deploy install options for renamed embedder runtime field.
crates/aube/src/commands/ci.rs Updates CI install options for renamed embedder runtime field.
crates/aube/src/commands/add/mod.rs Updates embed-facing add options to accept EmbedderRuntime.
crates/aube-scripts/src/lib.rs Splits NODE vs npm_node_execpath in script jail env stamping and supports extra embedder env.
crates/aube-node/src/lib.rs Updates Node-API host install options to the new runtime field.
crates/aube-ffi/src/lib.rs Updates FFI host install options to the new runtime field.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/aube/src/embed.rs Outdated
Comment on lines +410 to 415
pub fn child_env_vars() -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let ctx = current();
let execpath = ctx
.as_ref()
.and_then(|c| c.node_execpath.clone().or_else(|| c.node_program.clone()))
.or_else(aube_runtime::node_on_path);
Comment on lines +11 to +16
// Fold an embedder's `NODE_OPTIONS` contribution over the resolved
// `nodeOptions` setting (append by default, so a wrapper's `--import`
// preload adds to the user's value rather than replacing it).
let node_options = crate::runtime::merge_node_options(
aube_settings::resolved::node_options(ctx).and_then(non_empty_string),
);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aube/src/commands/run.rs (1)

450-479: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Pass the resolved project directory into ensure_installed. ensure_installed has no directory parameter and starts from crate::dirs::cwd(), while these embedding entry points resolve cwd/effective_cwd from an explicit base_dir. Those calls can therefore evaluate auto-install freshness against the wrong tree; thread the already-resolved project directory into the helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube/src/commands/run.rs` around lines 450 - 479, The resolved project
directory is not passed to ensure_installed, causing freshness checks to use the
process cwd instead of the requested project tree. Update ensure_installed and
every affected caller to accept and use the resolved directory, including the
run.rs flow around load_manifest and the exec.rs call site at lines 120-127;
pass cwd/effective_cwd rather than recomputing it from crate::dirs::cwd().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aube/src/commands/node.rs`:
- Around line 64-92: Update run_spawn to await
crate::process_guard::spawn_and_wait(cmd) instead of cmd.status().await,
preserving the existing diagnostic mapping and node spawn error context. Keep
the command configuration and returned status-code behavior unchanged.

In `@crates/aube/src/commands/script_settings.rs`:
- Line 54: Preserve each embedder environment entry’s EnvMerge mode when
constructing extra_env in crates/aube/src/commands/script_settings.rs:54-54. In
crates/aube-scripts/src/lib.rs:695-702, apply Append to the resolved or
inherited environment value and use Replace as an override, while retaining the
existing NODE_OPTIONS behavior.

In `@crates/aube/src/engines.rs`:
- Around line 111-118: Update resolve_node_version and its OnceLock<PROBED>
caching so probe_node_version is not reused across different active runtime
contexts. Key the cached result by the current node_execpath (or
effective_node_version(None)), or invalidate it whenever that runtime context
changes, while preserving the existing override and explicitly cached
runtime-version paths.

In `@crates/aube/src/runtime.rs`:
- Around line 410-444: Update child_env_vars() to accumulate EnvMerge::Append
values per key while iterating ctx.env, using the previously computed value as
the base for subsequent appends instead of re-reading std::env::var_os. Emit one
final entry per key, preserving Replace semantics and ensuring multiple appends
such as NODE_OPTIONS compose in order.

---

Outside diff comments:
In `@crates/aube/src/commands/run.rs`:
- Around line 450-479: The resolved project directory is not passed to
ensure_installed, causing freshness checks to use the process cwd instead of the
requested project tree. Update ensure_installed and every affected caller to
accept and use the resolved directory, including the run.rs flow around
load_manifest and the exec.rs call site at lines 120-127; pass cwd/effective_cwd
rather than recomputing it from crate::dirs::cwd().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: e39dcfad-fd48-486a-9915-54c656bcdf6c

📥 Commits

Reviewing files that changed from the base of the PR and between 3bfd1ec and e0911cb.

📒 Files selected for processing (19)
  • crates/aube-ffi/src/lib.rs
  • crates/aube-node/src/lib.rs
  • crates/aube-scripts/src/lib.rs
  • crates/aube/src/commands/add/mod.rs
  • crates/aube/src/commands/ci.rs
  • crates/aube/src/commands/deploy/mod.rs
  • crates/aube/src/commands/dlx.rs
  • crates/aube/src/commands/doctor.rs
  • crates/aube/src/commands/exec.rs
  • crates/aube/src/commands/install/args.rs
  • crates/aube/src/commands/install/mod.rs
  • crates/aube/src/commands/node.rs
  • crates/aube/src/commands/run.rs
  • crates/aube/src/commands/runtime.rs
  • crates/aube/src/commands/script_settings.rs
  • crates/aube/src/embed.rs
  • crates/aube/src/engines.rs
  • crates/aube/src/runtime.rs
  • docs/embedding/rust.md

Comment thread crates/aube/src/commands/node.rs
.as_ref()
.and_then(|r| r.node_execpath.clone().or_else(|| r.node_program.clone()))
.or_else(aube_runtime::node_on_path),
extra_env: crate::runtime::embedder_extra_env(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve EnvMerge for lifecycle-script environment entries.

embedder_extra_env() discards each entry’s merge mode, and the script layer consequently overwrites every non-NODE_OPTIONS key. Thus EmbedderRuntime::env_append("FOO", "bar") does not append for lifecycle scripts, despite its “every spawn” contract; only the special NODE_OPTIONS path works.

  • crates/aube/src/commands/script_settings.rs#L54-L54: retain EnvMerge alongside each embedder environment entry.
  • crates/aube-scripts/src/lib.rs#L695-L702: apply Append against the resolved/inherited value and Replace as an override.
📍 Affects 2 files
  • crates/aube/src/commands/script_settings.rs#L54-L54 (this comment)
  • crates/aube-scripts/src/lib.rs#L695-L702
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube/src/commands/script_settings.rs` at line 54, Preserve each
embedder environment entry’s EnvMerge mode when constructing extra_env in
crates/aube/src/commands/script_settings.rs:54-54. In
crates/aube-scripts/src/lib.rs:695-702, apply Append to the resolved or
inherited environment value and use Replace as an override, while retaining the
existing NODE_OPTIONS behavior.

Comment thread crates/aube/src/engines.rs Outdated
Comment thread crates/aube/src/runtime.rs
Two accommodations surfaced by studying nub's production fork (the
wrapper embedder from discussion #1083):

- EmbedderRuntime::internal_node: splits the node aube's internal
  machinery spawns (pnpmfile hooks, security scanner, version probes)
  from the wrapper shim, so a wrapping host can keep aube's hot paths
  on the real binary while user-facing spawns stay wrapped. Defaults
  to node_program (no behavior change unless set).
- embed::{run,exec,dlx,node} gain an optional per-call EmbedderRuntime,
  scoped via the task-local runtime slot. set_embedder_runtime is
  set-once, so hosts that provision a fresh shim dir per command (nub
  does) need per-call granularity on the non-install paths too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 23, 2026 20:49

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 4 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ff297dc. Configure here.

Comment thread crates/aube/src/runtime.rs
Comment thread crates/aube/src/runtime.rs Outdated
Comment thread crates/aube/src/commands/exec.rs
Comment thread crates/aube/src/commands/exec.rs Outdated
Comment thread crates/aube/src/runtime.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

…env merge

Review feedback from the bots on #1086:

- ensure_installed_in: embed::run/exec resolve the project from an
  explicit base_dir, but the auto-install freshness check anchored at
  the process cwd and installed the wrong tree. Thread the resolved dir
  through and target the auto-install at it.
- current(): a process-wide embedder runtime is now authoritative over a
  RUNTIME an earlier ensure() may have populated, so a late
  set_embedder_runtime can't leave dlx/exec/run/node on a
  pre-registration runtime while install scopes see the new one.
- engines: version probe is memoized per program path, so a runtime that
  became active after an earlier bare-node probe gets its own binary
  probed instead of a stale cached result.
- fold_env: embedder env now accumulates — multiple appends to one key
  chain in order, a later append composes onto a prior replace, and the
  jailed script path preserves append/replace semantics instead of
  flattening to replace.
- node::run_spawn uses process_guard::spawn_and_wait so an embedded Node
  child is torn down with the host instead of orphaned.
- Drop EnvMerge from the embed re-export (hosts use env_append/env_set);
  document dlx's process-cwd switch; refresh a stale node_exe comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 23, 2026 21:01

jdx commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review in a93ee82.

Fixed:

  • Auto-install targeted the wrong tree (greptile P1, coderabbit Major). embed::run/embed::exec resolve the project from an explicit dir, but ensure_installed anchored at the process cwd — so the freshness check and any auto-install hit whatever tree the host process sat in. Added ensure_installed_in(no_install, base_dir), threaded the resolved dir through the run/exec call sites, and pointed the auto-install InstallOptions.project_dir at it.
  • Late registration split runtime state (greptile P1). current()'s non-install branch now prefers EMBEDDER_CONTEXT over RUNTIME, so a set_embedder_runtime after an earlier ensure can't leave dlx/exec/run/node on a pre-registration runtime.
  • Memoized version ignored the embedder (cursor, coderabbit). The node --version probe is now memoized per program path instead of a single process-global slot, so a runtime that became active after an earlier bare-node probe gets its own node_execpath probed.
  • Env merge didn't accumulate (coderabbit ×2). New pure fold_env helper: multiple Appends to one key chain in order, a later Append composes onto a prior Replace, and the jailed script path preserves append/replace semantics rather than flattening to replace. Unit-tested directly.
  • run_spawn orphaned the child (coderabbit). Now goes through process_guard::spawn_and_wait, so an embedded Node child is torn down with the host like dlx/exec children.
  • Smaller API surface (copilot): dropped EnvMerge from the embed re-export (hosts use env_append/env_set); refreshed the stale node_exe comment; added the child_env_vars/fold_env append test copilot asked for.

Deliberately not changed:

  • dlx switches process cwd across an await (greptile P1). This is inherent to how dlx works — it chdirs into its scratch project for the transient install. Rather than rewrite dlx, I documented the caveat on embed::dlx: don't run it concurrently with other cwd-sensitive work in the same process. run/exec (the common embedding paths) are cwd-independent.

This comment was generated by Claude.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

crates/aube/src/engines.rs:139

  • This expect("probe cache poisoned") can panic in embedded/library contexts. Prefer recovering from poisoning by taking the inner guard.

This comment was generated by GitHub Copilot.

        .expect("probe cache poisoned")

Comment on lines +399 to +408
match runtime {
Some(rt) => {
scope(async move {
seed_install_embedder_runtime(Some(&rt));
future.await
})
.await
}
None => future.await,
}
Comment thread crates/aube/src/engines.rs Outdated
std::sync::Mutex<std::collections::HashMap<std::path::PathBuf, Option<String>>>,
> = std::sync::OnceLock::new();
let cache = PROBED.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
if let Some(cached) = cache.lock().expect("probe cache poisoned").get(program) {
…isoned probe lock

Second review round on #1086:

- Per-call embed runtime now rides a dedicated PER_CALL_RUNTIME
  task-local that survives the fresh scope install::run opens, so a
  wrapper passed to embed::dlx / embed::run reaches lifecycle scripts in
  the nested (transient / auto-) install instead of being dropped
  (cursor, High).
- with_embedder_runtime always opens its own runtime scope, so an embed
  call resolves Node into an isolated slot rather than reusing the
  process-wide RUNTIME an earlier call populated — sequential calls to
  different projects no longer cross-contaminate (cursor / copilot).
- engines version-probe cache recovers from a poisoned lock
  (into_inner) instead of expect()-panicking, since it runs in
  embedded/library contexts (copilot).

Earlier-round items (auto-install cwd anchoring, late-registration
split, per-path version memo, env-merge accumulation) were already
addressed in a93ee82; the re-flags were against pre-fix commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 00:13

jdx commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Second review round addressed in b9ee8bc.

Fixed (still-valid against the latest commit):

  • Per-call runtime dropped in nested installs (cursor, High). with_embedder_runtime seeded only the outer scope's slot, but install::run opens a fresh scope, so a wrapper passed to embed::dlx / embed::run was lost for lifecycle scripts in the transient/auto-install. Moved the per-call runtime onto a dedicated PER_CALL_RUNTIME task-local that isn't re-scoped by scope, and had seed_install_embedder_runtime consult it. New test per_call_runtime_reaches_a_nested_install_scope.
  • Sequential embed calls reused the first runtime (cursor "cached runtime ignores later project_dir", copilot). with_embedder_runtime now always opens its own scope, so each call resolves Node into an isolated slot instead of the process-wide RUNTIME — embed::run(projectA) then embed::run(projectB) no longer makes B inherit A's resolution.
  • Poisoned-lock panic in the version-probe cache (copilot). Recovers via into_inner() instead of expect(), since it runs in embedded/library contexts; a stale entry is harmless.

Already fixed in a93ee82 — the re-flags were against pre-fix commits (ff297dc/e0911cb):

  • Auto-install cwd anchoring (greptile P1 exec.rs:127, cursor exec.rs:125) → ensure_installed_in(base_dir).
  • Late-registration split (greptile P1 runtime.rs:361) → current() prefers EMBEDDER_CONTEXT.
  • child_env_vars append accumulation (coderabbit runtime.rs:546) → fold_env.

Still standing by design: embed::dlx changing process cwd during its transient install (greptile P1) — inherent to dlx; documented on the entry point. run/exec are cwd-independent.

674 lib tests + 19 engines tests pass; clippy/fmt clean.

This comment was generated by Claude.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

crates/aube/src/runtime.rs:437

  • with_embedder_runtime(None, ...) opens a fresh scope, which makes current() consult only INSTALL_RUNTIME. Because the slot starts empty and nothing seeds it from EMBEDDER_CONTEXT, embed entrypoints like embed::run/exec/dlx/node won’t honor a process-wide set_embedder_runtime(...) (they’ll fall back to resolving their own runtime via ensure_for_cwd). Seed the scoped slot from the process-wide embedder runtime (when present) before running the future.

This comment was generated by GitHub Copilot.

        None => scope(future).await,

b9ee8bc made with_embedder_runtime always open an isolation scope so
sequential embed calls don't reuse each other's resolved runtime — but
the `None` arm opened the scope without seeding it. Inside a scope,
`ensure` resolves into the empty slot and `current()`'s process-wide
fallbacks are never consulted, so a *globally* registered
set_embedder_runtime was silently bypassed on embed::{run,exec,dlx,node}
— dependencies ran unwrapped, the exact failure mode #1083 exists to
prevent.

- with_embedder_runtime: both arms now seed the fresh scope through
  seed_install_embedder_runtime(None) (per-call -> process-wide ->
  none); the Some arm publishes PER_CALL_RUNTIME and relies on the same
  chain instead of a duplicated inline set.
- ensure: the no-scope branch prefers EMBEDDER_CONTEXT over RUNTIME, so
  a late registration can't hand callers a stale pre-registration
  context; the now-redundant (and silently-failing) RUNTIME pre-fill in
  set_embedder_runtime is gone.
- scope_current: also propagates PER_CALL_RUNTIME, since task-locals
  don't cross tokio::spawn.
- Regression test: none_arm_still_seeds_from_the_ambient_embedder_runtime
  (exercised via the per-call tier — the process-wide global is
  once-per-process and would leak across unit tests; both tiers flow
  through the same seed path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 00:22

jdx commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

On a closer pass over b9ee8bc I found — and fixed in e3c6cd2 — a regression that commit introduced.

The bug. b9ee8bc made with_embedder_runtime always open an isolation scope so sequential embed calls don't reuse each other's resolved runtime. But the None arm opened the scope without seeding it. Inside a scope, current()'s Ok branch is authoritative (the process-wide fallbacks live only in the no-scope branch), so ensure resolved aube's own runtime into the empty slot — and a globally registered set_embedder_runtime was silently bypassed on embed::{run, exec, dlx, node}. Dependencies would run unwrapped: the exact failure mode #1083 calls out as "worse than not wrapping".

The fix. Both arms of with_embedder_runtime now seed the fresh scope through seed_install_embedder_runtime(None) — one lookup chain (per-call → process-wide → none) instead of a duplicated inline set. Two adjacent hardenings in the same commit:

  • ensure's no-scope branch prefers EMBEDDER_CONTEXT over RUNTIME, so a late registration can't hand callers a stale pre-registration context; the now-redundant, silently-failing RUNTIME pre-fill in set_embedder_runtime is gone (this also fully retires greptile's "late registration splits runtime state").
  • scope_current also propagates the per-call runtime, since task-locals don't cross tokio::spawn — unreachable via the embed entry points today (they use empty filters), but the correct semantics for any future fan-out.

Test. none_arm_still_seeds_from_the_ambient_embedder_runtime fails on the pre-fix code (empty slot → current() is None inside the scope). It exercises the seed chain via the per-call tier rather than the process-wide global, deliberately: set_embedder_runtime is once-per-process, and setting it in the shared unit-test binary would leak into unrelated tests that spawn internal_node_program() outside any scope (pnpmfile / security-scanner tests). Both tiers flow through the same seed_install_embedder_runtime(None) path.

675 lib tests pass; clippy/fmt clean.

This comment was generated by Claude.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Comment on lines +325 to 327
if let Some(bin) = &ctx.node_program {
println!(" bin: {}", bin.display());
}
Comment on lines +172 to 174
if let Some(bin) = &ctx.node_program {
s.push("node-bin", display_path_owned(bin.clone()));
}
@colinhacks

colinhacks commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Looks good! This'll work for Nub perfectly as-is.

@jdx
jdx merged commit 57102a8 into main Jul 24, 2026
38 checks passed
@jdx
jdx deleted the claude/github-discussion-1083-860e25 branch July 24, 2026 21:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants