Skip to content

feat(compile): add Node.js runtime extension with optional internal-feed config#398

Draft
Copilot wants to merge 2 commits intofeat/runtime-prompt-injectionfrom
copilot/implement-node-runtime-extension
Draft

feat(compile): add Node.js runtime extension with optional internal-feed config#398
Copilot wants to merge 2 commits intofeat/runtime-prompt-injectionfrom
copilot/implement-node-runtime-extension

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 5, 2026

Summary

Implements a Node.js runtime extension for the runtimes: node: front-matter field, following the same pattern as the existing Lean 4 runtime.

What it does

When runtimes: node: is enabled in an agent's front matter, the compiler:

  1. Injects a NodeTool@0 step into {{ prepare_steps }} (runs before the agent), using the shared node_tool_step() helper introduced in feat(compile): runtime prompt injection via prompt.js bundle #395. NodeTool@0 is idempotent, so it is safe even when prompt.js also emits the step.
  2. Adds node, npm, npx to the bash command allow-list.
  3. Adds the "node" ecosystem to the AWF network allowlist (expands to registry.npmjs.org, nodejs.org, etc.).
  4. Appends a prompt supplement informing the agent that Node.js is available.
  5. Emits a warning if tools.bash is empty (Node.js requires bash access).

With optional internal-feed configuration, an additional bash step is injected that runs npm config set registry to redirect npm commands to a private registry (e.g., Azure Artifacts). When auth-token-var is also set, the step configures the per-registry _authToken using a value read from a pipeline variable at runtime — it is never embedded in the compiled YAML.

Usage

# Simple enablement (installs Node.js 20.x LTS)
runtimes:
  node: true

# Pin to a specific LTS major version
runtimes:
  node:
    version: "22.x"

# With an internal npm feed (Azure Artifacts)
runtimes:
  node:
    version: "20.x"
    internal-feed:
      registry: "https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/npm/registry/"
      auth-token-var: "SC_READ_TOKEN"

Files changed

  • src/compile/extensions/mod.rsnode_tool_step() now accepts a version_spec: &str parameter; updated two existing callers to pass "20.x" explicitly.
  • src/runtimes/node/mod.rs (new)NodeRuntimeConfig, NodeOptions, NodeInternalFeedConfig, NODE_BASH_COMMANDS, generate_node_install(), generate_node_feed_config().
  • src/runtimes/node/extension.rs (new)NodeExtension implementing CompilerExtension.
  • src/runtimes/mod.rs — added pub mod node;.
  • src/compile/types.rs — added node field to RuntimesConfig; updated sanitize_config_fields.
  • src/compile/extensions/mod.rs — re-exported NodeExtension; added Node(NodeExtension) to Extension enum; wired collect_extensions.
  • docs/runtimes.md — documented runtimes.node with examples and an internal-feed option table.
  • AGENTS.md — added node/ to the source tree overview.

Tests

  • cargo build
  • cargo test — 1117 + 78 + others pass; no regressions
  • cargo clippy --all-targets --all-features — clean

Copilot AI and others added 2 commits May 5, 2026 08:01
Copilot AI requested a review from jamesadevine May 5, 2026 08:07
jamesadevine added a commit that referenced this pull request May 5, 2026
* feat(runtimes): add unified Node.js and Python runtime extensions

Add Python and Node.js runtimes with consistent architecture matching
the existing Lean runtime pattern:

- Python: UsePythonVersion@0, PipAuthenticate@1, PIP_INDEX_URL/UV_DEFAULT_INDEX env vars
- Node.js: NodeTool@0 (inline, decoupled from ado-script), npmAuthenticate@0, NPM_CONFIG_REGISTRY env var
- Both use flat feed-url: field with env var injection via agent_env_vars()
- Both accept config: field (recognized but errors if used, reserved for AWF proxy-auth)
- Shared validate_feed_url() in validate.rs for injection checks
- agent_env_vars() trait method on CompilerExtension with BLOCKED_ENV_KEYS validation
- No AWF mounts/PATH prepends needed (hostedtoolcache auto-mounted by AWF)

Unifies the approaches from PRs #398 and #399 into a single consistent implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(runtimes): ensure .npmrc exists before npmAuthenticate@0

npmAuthenticate@0 requires workingFile to point at an existing file,
unlike PipAuthenticate@1. Emit a bash step that creates a minimal
.npmrc (with the configured registry or default npmjs) when one does
not already exist, preserving any repo-checked-in .npmrc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(runtimes): fix dead-code validation ordering, accept node config with warning

- Python: swap mutual-exclusivity check before not-yet-supported error
  so both paths are reachable
- Node: accept config: with a warning that .npmrc won't be available
  inside AWF yet (instead of hard error), check mutual exclusivity first
- Add tests for mutual-exclusivity errors on both runtimes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(runtimes): reject double-quote in feed URLs and agent env var values

A double-quote in a feed-url or extension env var value would produce
malformed YAML in the generated pipeline (the value is emitted as
KEY: "value"). Reject at validation time in both validate_feed_url()
and collect_agent_env_vars() with clear error messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(runtimes): Python config: produces warning instead of error

Align with Node.js behavior — accept config: with a warning that the
config file will not be available inside the AWF agent environment yet,
rather than a hard compile error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(runtimes): make auth tasks conditional on feed-url or config

PipAuthenticate@1 and npmAuthenticate@0 (plus ensure-npmrc) are now
only emitted when feed-url: or config: is set. Users who enable
runtimes: python: true or runtimes: node: true without an internal
feed no longer get unnecessary auth steps in their pipeline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(runtimes): reject single-quote in feed URLs, fix doc comments, soften uv prompt

Three fixes:

1. validate_feed_url() now rejects single-quote characters alongside
   double-quotes — a single quote in the feed URL would break the bash
   single-quoted string in generate_ensure_npmrc's echo command.

2. Doc comments on generate_pip_authenticate() and
   generate_npm_authenticate() corrected to say "emitted when feed-url
   or config is set" instead of "emitted unconditionally", matching the
   actual conditional behavior in prepare_steps().

3. Python prompt supplement no longer claims uv is "pre-installed" —
   ADO hosted runners don't ship uv. Now says "install it first with
   pip install uv" to avoid command-not-found errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(runtimes): harden env var collection, fix docs and bash quoting

Security & correctness fixes:

1. collect_agent_env_vars now calls reject_pipeline_injection (covers
   ADO expressions, pipeline commands, template markers, newlines)
   instead of only contains_pipeline_command. Also rejects single
   quotes alongside double quotes.

2. collect_agent_env_vars now deduplicates env var keys — bails on
   collision instead of silently emitting duplicate YAML keys.

3. generate_ensure_npmrc diagnostic echo lines switched from
   double-quotes to single-quotes, preventing ${VAR} shell expansion
   if a feed URL contained that pattern.

4. docs/runtimes.md corrected: auth tasks are conditional on feed-url
   or config being set, not unconditional. Config field descriptions
   updated to reflect warning-not-error behavior. Added note about
   PipAuthenticate@1 empty artifactFeeds limitation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(runtimes): PipAuthenticate requires feed-url, not config alone

PipAuthenticate@1 with empty artifactFeeds doesn't authenticate to any
specific feed. Only emit it when feed-url is set — config alone is not
sufficient since the config file won't be available in AWF yet anyway.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants