Skip to content

feat(runtimes): add dotnet runtime extension#435

Merged
jamesadevine merged 4 commits into
mainfrom
copilot/add-dotnet-runtime-compiler-extension
May 7, 2026
Merged

feat(runtimes): add dotnet runtime extension#435
jamesadevine merged 4 commits into
mainfrom
copilot/add-dotnet-runtime-compiler-extension

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 7, 2026

Summary

Adds a dotnet runtime alongside the existing python and node runtimes, following the same CompilerExtension pattern but adapted to NuGet's nuget.config-centric conventions and to .NET's global.json SDK pinning.

runtimes:
  dotnet:
    version: "8.0.x"
    feed-url: "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json"

# Or pin the SDK from the repo's global.json
runtimes:
  dotnet:
    version: "global.json"

Runtime (src/runtimes/dotnet/)

  • versionUseDotNet@2 with packageType: 'sdk' (default 8.0.x)
  • version: "global.json" (case-insensitive) → emits useGlobalJson: true (no explicit version: input), so the install task discovers and installs every SDK referenced by global.json files in the workspace
  • feed-url → ensure-nuget.config shim (writes a minimal config only if none exists, probing the three case variants NuGet recognises) + NuGetAuthenticate@1
  • configNuGetAuthenticate@1 only; trusts the checked-in file
  • feed-url / config are mutually exclusive; both run through validate_feed_url / reject_pipeline_injection
  • Adds dotnet to the bash allow-list, the dotnet ecosystem domains to the AWF allowlist, and a prompt supplement
  • The ensure-nuget.config shim's confirmation echo uses single quotes (matching the node runtime convention) so any characters that slip past validate_feed_url cannot be shell-expanded at agent runtime

global.json precedence (single source of truth)

  • If a global.json exists at the agent's compile directory and the front matter sets a concrete version, compilation errors out with a message pointing the user at either removing the version or using the "global.json" sentinel.
  • version: "global.json" + on-disk global.json is the intended happy path.
  • No-version + on-disk global.json is allowed (default 8.0.x is used) — users opt into useGlobalJson explicitly with the sentinel rather than auto-promoting.
  • To support workspace probing from runtime validators, compile_dir: Option<&Path> is now plumbed through CompileContext (with a matching for_test_with_compile_dir test helper).

Wiring

  • RuntimesConfig.dotnet field, Extension::Dotnet enum variant, collect_extensions branch
  • Updated the four pre-existing RuntimesConfig { … } literal sites to include the new field

Tests & docs

  • 18 dotnet tests in compile/extensions/tests.rs (12 mirroring python/node coverage — enable/disable/with-version, hosts, bash commands, prepare_steps with/without feed-url, config-only, env-vars contract, mutual exclusivity, injection rejection, all-runtimes-enabled — plus 6 new global.json tests: sentinel emits useGlobalJson, case-insensitive, sentinel skips injection check, version+on-disk-global.json conflict-errors, sentinel+on-disk-global.json OK, no-version+on-disk-global.json OK)
  • New ### .NET section in docs/runtimes.md (with a global.json precedence subsection) and a commented example in docs/front-matter.md

Required differences from python/node (called out in docs/runtimes.md)

  • No agent env var for feed-url. NuGet has no first-class equivalent of PIP_INDEX_URL / NPM_CONFIG_REGISTRY; the convention is nuget.config. agent_env_vars() returns empty (pinned by a test).
  • config: is functional, not a deferred warning. AWF only overlays files in $HOME (e.g., ~/.npmrc/dev/null); workspace files such as nuget.config are preserved inside the agent sandbox, so a checked-in nuget.config works today — no gh-aw-firewall#2547-style warning needed. The runtimes.dotnet.config value still runs through reject_pipeline_injection as defence-in-depth, even though it is not currently embedded in any generated YAML.
  • NuGetAuthenticate@1 needs no workingFile: — it auto-discovers nuget.config anywhere in the workspace, unlike npmAuthenticate@0.
  • Display name is dotnet, not .NET. The existing wrap_prompt_append runtime guard rejects . in extension names. (The same guard would currently reject the existing Node.js extension if exercised end-to-end — pre-existing, not addressed here.)
  • global.json sentinel + on-disk conflict detection. No analogue exists in python/node since global.json is .NET-specific.

Test plan

  • cargo build, cargo test (1203 lib tests pass, 18 dotnet-specific), cargo clippy --all-targets --all-features — no new warnings
  • End-to-end: compiled a sample agent with runtimes.dotnet + feed-url and verified the generated YAML emits UseDotNet@2, the ensure-nuget.config heredoc, NuGetAuthenticate@1, the dotnet ecosystem domains in the AWF --allow-domains list, and dotnet on the bash allow-list

@jamesadevine
Copy link
Copy Markdown
Collaborator

@copilot Repos can have a global.json which should definitely override the specified version in the frontmatter. This would then be used in the install task. I think there is a flag like useGlobalJson: true in the task. We should detect the presence of the global.json as a compiler error if the frontmatter specified a version. we should allow a special version specifier called global.json or similar that will take the version from global.json always by setting useglobalJson to true. Double check my assertions here.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 7, 2026

@copilot Repos can have a global.json which should definitely override the specified version in the frontmatter. This would then be used in the install task. I think there is a flag like useGlobalJs...

Confirmed your assertions against the Microsoft Learn docs for UseDotNet@2useGlobalJson: true is the right knob, and when set the explicit version input is ignored. Implemented in 95888df:

  • version: "global.json" (case-insensitive) → emits useGlobalJson: true (no version: input) with displayName Install .NET SDK (from global.json).
  • If runtimes.dotnet.version is set to a concrete value AND a global.json exists at the agent's compile dir, compilation now errors out, telling the user to either drop the version or set it to "global.json".
  • No-version + on-disk global.json is still allowed (default 8.0.x is used) — users opt into useGlobalJson explicitly with the sentinel rather than auto-promoting.
  • Added 6 new tests; compile_dir is now plumbed through CompileContext so validators can probe the workspace.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • spsprodeus21.vssps.visualstudio.com
    • Triggering command: /home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/ado_aw-2a3257b677f558ec /home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/ado_aw-2a3257b677f558ec .d8b28416bc2ef292-cgu.1.rcgu.o .d8b28416bc2ef292-cgu.2.rcgu.o .d8b28416bc2ef292-cgu.3.rcgu.o .d8b28416bc2ef292-cgu.4.rcgu.o .d8b28416bc2ef292-cgu.5.rcgu.o .d8b28416bc2ef292-cgu.6.rcgu.o .d8b28416bc2ef292-cgu.7.rcgu.o 448c33a57a713757cc wcff18zwoitmvls8pk8.rcgu.o eta.o 448c33a57a713757-m64 64-u�� 64-REDACTED-linux-gnu/lib/librustc_literal_escaper-1ebe8258df6cf91a.rlib 64-REDACTED-linux-gnu/lib/libstd-46d936097e8c5b85.rlib 270dc1a2/build_s/home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/rmcp_macros-f0fd491bffe7ae8a.r/home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/rustc6lbNHJ/symbols.o -4f0f17ff7b9c793grep 8aa98-cgu.0.rcgu-E lib/rustlib/x86_test_dotnet|test result|^error|^failures|FAILED lib/rustlib/x86_/home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/rmcp_macros-f0fd491bffe7ae8a.r/home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/ado_aw-4e74e1c2f7c8386c.04mc94w5hl3s9ihz93d0zzlr3.0rmm3zm.rcgu.o (dns block)
  • spsprodweu4.vssps.visualstudio.com
    • Triggering command: /home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/ado_aw-2a3257b677f558ec /home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/ado_aw-2a3257b677f558ec .d8b28416bc2ef292-cgu.1.rcgu.o .d8b28416bc2ef292-cgu.2.rcgu.o .d8b28416bc2ef292-cgu.3.rcgu.o .d8b28416bc2ef292-cgu.4.rcgu.o .d8b28416bc2ef292-cgu.5.rcgu.o .d8b28416bc2ef292-cgu.6.rcgu.o .d8b28416bc2ef292-cgu.7.rcgu.o 448c33a57a713757cc wcff18zwoitmvls8pk8.rcgu.o eta.o 448c33a57a713757-m64 64-u�� 64-REDACTED-linux-gnu/lib/librustc_literal_escaper-1ebe8258df6cf91a.rlib 64-REDACTED-linux-gnu/lib/libstd-46d936097e8c5b85.rlib 270dc1a2/build_s/home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/rmcp_macros-f0fd491bffe7ae8a.r/home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/rustc6lbNHJ/symbols.o -4f0f17ff7b9c793grep 8aa98-cgu.0.rcgu-E lib/rustlib/x86_test_dotnet|test result|^error|^failures|FAILED lib/rustlib/x86_/home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/rmcp_macros-f0fd491bffe7ae8a.r/home/REDACTED/work/ado-aw/ado-aw/target/debug/deps/ado_aw-4e74e1c2f7c8386c.04mc94w5hl3s9ihz93d0zzlr3.0rmm3zm.rcgu.o (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from jamesadevine May 7, 2026 11:26
@jamesadevine jamesadevine marked this pull request as ready for review May 7, 2026 12:44
@jamesadevine
Copy link
Copy Markdown
Collaborator

/rust-review

@jamesadevine
Copy link
Copy Markdown
Collaborator

@copilot Repos can have a global.json which should definitely override the specified version in the frontmatter. This would then be used in the install task. I think there is a flag like useGlobalJs...

Confirmed your assertions against the Microsoft Learn docs for UseDotNet@2useGlobalJson: true is the right knob, and when set the explicit version input is ignored. Implemented in 95888df:

  • version: "global.json" (case-insensitive) → emits useGlobalJson: true (no version: input) with displayName Install .NET SDK (from global.json).
  • If runtimes.dotnet.version is set to a concrete value AND a global.json exists at the agent's compile dir, compilation now errors out, telling the user to either drop the version or set it to "global.json".
  • No-version + on-disk global.json is still allowed (default 8.0.x is used) — users opt into useGlobalJson explicitly with the sentinel rather than auto-promoting.
  • Added 6 new tests; compile_dir is now plumbed through CompileContext so validators can probe the workspace.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

That'll do for now - we need to iron out config files in the future, but TODO!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

Rust PR Reviewer completed successfully!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🔍 Rust PR Review

Summary: Looks good overall — well-structured extension following the established pattern, thorough test coverage, correct validation. Two minor issues worth addressing.

Findings

🔒 Security Concerns

  • src/runtimes/dotnet/mod.rs:222generate_ensure_nuget_config echoes the feed URL inside double quotes:
    echo \"Created nuget.config with source={feed_url}\"
    The {feed_url} is inlined by Rust's format!, but in the generated bash script any bare $IDENTIFIER or backtick patterns in that literal string will be expanded at runtime. validate_feed_url blocks $(...) ADO expressions but not bare $VAR references or backtick command substitution. The node runtime uses single-quoted echo throughout (lines 161-162 of node/mod.rs), which is immune to all shell expansion. Fix: change to echo 'Created nuget.config with source={feed_url}'.

⚠️ Suggestions

  • src/runtimes/dotnet/extension.rs — validate block for config — the comment says:

    the path itself is user-supplied and ends up in displayName/log output

    But generate_nuget_authenticate() takes no arguments and emits a fixed string — the config value is never embedded anywhere in the generated YAML. The comment is misleading for future contributors (it reads like there's an injection risk that exists, when the value is simply never emitted). Either remove the comment or add the field to a future displayName.

✅ What Looks Good

  • Heredoc uses <<'EOF' (single-quoted) — the XML content including the feed URL is fully protected from shell expansion at runtime. Correct.
  • validate_feed_url is called on feed-url (blocks ", ', $(, ##vso[, newlines) while the simpler reject_pipeline_injection is correctly used for version (which doesn't go through XML or echo).
  • global.json sentinel logic is clean: case-insensitive comparison, injection check skipped for the literal keyword (tested), and the on-disk conflict detection correctly uses ctx.compile_dir which is always Some in production paths.
  • Mutual exclusivity between config and feed-url is enforced and tested.
  • compile_dir: Option<&'a Path> threading through CompileContext is a clean approach that keeps on-disk probing test-friendly via for_test_with_compile_dir.
  • 18 dedicated tests with good boundary coverage.

Generated by Rust PR Reviewer for issue #435 · ● 1.3M ·

@jamesadevine
Copy link
Copy Markdown
Collaborator

@copilot implement suggestions and fixes for bug

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 7, 2026

@copilot implement suggestions and fixes for bug

Addressed both rust-review findings in 91cbe6c:

  • Switched the Created nuget.config with source={feed_url} echo to single quotes in src/runtimes/dotnet/mod.rs, matching the node runtime convention. With double quotes, any $VAR / backtick characters that slipped past validate_feed_url (which only blocks $(, ADO ##vso[, quotes and newlines) could have been expanded by bash at agent runtime.
  • Rewrote the comment above validate::reject_pipeline_injection for runtimes.dotnet.config to accurately describe the check as defence-in-depth (the value is never currently embedded in the generated YAML — NuGetAuthenticate@1 auto-discovers nuget.config).

All 18 dotnet tests still pass; clippy reports no new warnings.

@jamesadevine jamesadevine merged commit bdfb21c into main May 7, 2026
1 check passed
@jamesadevine jamesadevine deleted the copilot/add-dotnet-runtime-compiler-extension branch May 7, 2026 13:33
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