Skip to content

feat(compile)!: credential-isolated package feed access via permissions.packages - #2315

Merged
jamesadevine merged 18 commits into
mainfrom
feat/package-feed-proxy
Oct 27, 2026
Merged

jamesadevine merged 18 commits into
mainfrom
feat/package-feed-proxy

Conversation

@jamesadevine

@jamesadevine jamesadevine commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Agents can now restore packages from internal Azure Artifacts feeds with npm,
pip/uv, dotnet, and cargo without ever holding a feed credential.
The trusted ado-proxy container, already used for permissions.read, gains
a second package family. It holds the feed credential and attaches it only
to GET/HEAD requests for the feeds, views, and protocols the workflow
grants. This is the follow-up to #2310 (merged), which removed the
*Authenticate tasks because they leaked the token into AWF. It addresses the
internal-feed half of #1823 and #253.

permissions:
  packages:
    service-connection: artifacts-reader   # optional; omit to use the build identity
    identity-role: reader
    feeds:
      - name: internal
        project: Engineering
        feed: internal-packages
        view: Release
        protocols: [npm, pypi, nuget]
runtimes:
  node:
    feed: internal
    public-registry: block

How it works

  • Credential custody.
    • Build identity: $(System.AccessToken) is mapped as a secret only into
      the proxy start step.
    • WIF: AzureCLI@3 mints SC_PACKAGES_TOKEN, which is secret and in
      --exclude-env.
    • Either way, the token is streamed to the proxy over stdin before AWF
      starts. It is never placed in the Agent or Detection environment, argv,
      workspace, or runner /tmp.
  • Separate slots.
    • The material document is now v2, with token (REST) and package_token
      (packages).
    • The REST bearer never reaches pkgs.dev.azure.com, and the package
      credential never reaches dev.azure.com.
    • Either family is inert when its permission is absent.
  • Policy. Requests must use GET/HEAD, a catalogued protocol route
    (npm/registry/, pypi/simple|download/, nuget/v3/, cargo/), and a
    granted org/project/feed/view tuple. Feed GUID aliases are resolved on the
    host before AWF starts.
  • Preflight. ado-proxy.js resolve-feeds also probes access. A failure
    stops the job with the feed, identity, and role to grant.
  • Redirects. The proxy never follows redirects. It relays Location only
    for *.vsblob.visualstudio.com and *.blob.core.windows.net, and the
    client fetches the signed blob URL through Squid with no credential.
  • Ingress.
    • Generated wrappers (npm, npx, pip, pip3, uv, dotnet, cargo)
      set HTTPS_PROXY and CA trust for that process only. CA trust is never
      installed container-wide.
    • Canonical pkgs.dev.azure.com URLs, including ones in repository config,
      work unchanged.
    • Bypassing a wrapper (for example python -m pip) fails closed with no
      credential.
  • Source selection.
    • runtimes.<x>.feed sets PIP_INDEX_URL/UV_DEFAULT_INDEX,
      NPM_CONFIG_REGISTRY, or an ensure-nuget.config step, all
      credential-free.
    • public-registry: block removes that ecosystem's public registry hosts
      from the AWF allowlist.
  • Upstream ingestion.
    • A Collaborator-role read can save an upstream package into the feed.
    • upstream: deny (the default) therefore requires either a view or
      identity-role: reader, otherwise compilation fails.
    • upstream: allow is an explicit opt-in.
  • Audit. Decision records gain optional family and protocol fields;
    the log stays v1-compatible. The changes add rollups and an
    upstream-unauthorized finding.

Breaking change and codemod

An Azure Artifacts runtimes.<x>.feed-url is replaced by
permissions.packages plus runtimes.<x>.feed. Codemod 0009 migrates it
automatically:

  • shared feeds are merged and handles deduplicated;
  • @view is kept;
  • upstream: allow is added when no view is present, preserving the build
    identity's previous behavior.

Non-Artifacts feed-url values are unchanged.

Deviations from the plan (for reviewers)

Topic Decision
Legacy {org}.pkgs.visualstudio.com Not intercepted; requests fail closed with no credential. Codemod 0009 rewrites front-matter URLs. For package config committed at the repo root (nuget.config, .npmrc, pip.conf, .cargo/config.toml, …), compile and ado-aw lint print a legacy-feed-url warning with the canonical replacement (45c3ca4c).
Cross-org feeds with the build identity Fail at runtime preflight instead of compile time, because the org is often unknown at compile time.
WIF mint Not deduplicated with permissions.read.
Python/Node config: Follow-up: still accepted with a warning and not wired into pip/uv/npm.
Checked-in config linting Done for the repository root: compile and ado-aw lint report legacy-feed-url (bb5c55eb) and ungranted-feed-url (1aa91802). The second covers feed, view, and protocol grants and mirrors the proxy's matching. See docs/package-feeds.md.
Cargo Proxy and wrapper only. There is no Cargo runtime, so the registry comes from repository .cargo/config.toml (#1823).

Live validation (AgentPlayground)

Two new candidate smoke cases run against a real Azure Artifacts feed
(AgentPlaygroundTestFeed):

  • package-feeds uses the build identity and blocks the public registries.
  • package-feeds-wif uses agent-playground-read via WIF.

Feed, upstreams and grants are recorded in tests/smoke/REGISTERED.md. A new
smokeCases runtime parameter on the candidate orchestrator lets a run cover
only named cases.

Full candidate run 647482 at b76ec48d (pre-rebase; the same tree is 4a36c005 after rebasing onto main): all 7 cases green, each with its proof tag.

Case Build Proof tag
package-feeds 647486 ado-aw-packages-647486
package-feeds-wif 647492 ado-aw-packages-wif-647492
ado-proxy (REST) 647490 ado-aw-proxy-647490
custom-safe-output 647488 ado-aw-custom-job-647488
canary, noop-target, multi-repo 647487, 647489, 647491 —

Together these runs prove:

  • no package credential reaches the sandbox;
  • npm pack, pip download and dotnet add package restore through the
    proxy, including the redirect to *.vsblob.vsassets.io;
  • both the build identity and an Entra (WIF) token authenticate with the
    per-protocol scheme the catalog selects;
  • an ungranted feed gets a 403;
  • public-registry: block holds.

Bugs the live runs found (all fixed here, with tests)

Finding Fix
Downloads redirect to *.vsblob.vsassets.io, which wasn't on the redirect allowlist 4296be59 adds the suffix to the catalog
The feed GET returns 200 even without ReadPackages, so the preflight passed for identities that couldn't restore 4296be59: the preflight now lists one package
Copilot CLI's shell(npm) allows only a bare npm, so every real npm/pip/dotnet/git call was refused #2325, merged to main separately
pip uses truststore, which ignores PIP_CERT a053a74c: the pip wrapper also sets SSL_CERT_FILE
NuGet sends HTTPS through http_proxy, which bypassed the engine 8b01b1b4: the dotnet wrapper also sets http_proxy
AWF's entrypoint prepends $DOTNET_ROOT and similar dirs, so the real dotnet shadowed the wrapper b21407d9: awf_path_prepends dirs are re-asserted first inside the engine command
AWF matches subdomains, so the apex npmjs.org still admitted registry.npmjs.org under public-registry: block 4a36c005: removal drops every entry that would admit a blocked host

Two earlier wrong diagnoses (231f9e22, 37531104) are reverted by
3d01595d.

The smokes pass --no-custom-instructions. Otherwise the checked-out
repository's AGENTS.md casts the agent as a contributor, and it declines the
task.

Review follow-ups from the first /review:

  • fb5496cc: exclusivity tests;

  • a strict streamed-body truncation e2e test.
    Docs:

  • new docs/package-feeds.md;

  • updated ado-proxy-design.md, runtimes.md, network.md, codemods.md,
    imports.md, front-matter.md, safe-output-permissions.md, AGENTS.md,
    and README.

Test plan

All run locally on Windows with shellcheck available:

  • cargo build: passed
  • cargo test --no-fail-fast: passed (3750 passed, 0 failed, 2 ignored)
  • cargo clippy --all-targets: passed (no warnings)
  • ENFORCE_BASH_LINT=1 cargo test --bin ado-aw compile::shell: passed (59)
  • ENFORCE_BASH_LINT=1 cargo test --test bash_lint_tests: passed (2)
  • cd scripts/ado-script && npm run codegen: passed; no drift beyond the
    committed catalog v2 artifacts
  • npm test: passed (97 files, 1369 tests, including new package,
    resolve-feeds, and proxy e2e cases for each protocol, denial, view, slot
    isolation, and canary absence)
  • npm run typecheck: passed
  • npm run build: passed
  • npx vitest run src/compiler-smoke-e2e: passed (292)
  • npm run build:compiler-smoke-e2e: passed
  • git diff --check: flags only README table rows that keep the file's
    existing CRLF endings

Live: full candidate smoke run 647482 green (see Live validation above). After rebasing onto main (which now holds #2310 and #2325), the branch tree is byte-identical to the one that was tested.

@jamesadevine

Copy link
Copy Markdown
Collaborator Author

/review

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
2 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

✅ TypeScript Code Quality Reviewer completed the TypeScript code quality review.

🟦 TypeScript code quality review by TypeScript Code Quality Reviewer

@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Security review: no regressions found

This diff adds the package-feed credential-isolation proxy (permissions.packages). I reviewed the full diff against the threat model (token custody, injection into generated YAML/shell, path traversal, redirect SSRF, network allowlisting, safe-output integrity) and found no weakened security posture.

Notable positives that satisfied my checks rather than raised concerns:

  • Credential isolation: the REST bearer and the new Azure Artifacts package credential are kept in separate material-document fields (token / package_token), only ever read from env/stdin into the trusted ado-proxy process, never written to disk, argv, or the Agent/Detection environment (ca.ts, agentic_pipeline.rs).
  • URL-shape authorization (package.ts) rejects path traversal, encoded/double-encoded separators, backslashes, control characters, and mismatched scope/view/protocol grants, each pinned by an explicit deny-reason test — including a dedicated test asserting denial details never echo request content.
  • Redirect handling (authorizePackageRedirect) never follows a redirect while holding the credential; it only relays to an allowlisted, suffix-anchored blob-storage host (.blob.core.windows.net, .vsblob.visualstudio.com) or a same-host route that is itself re-authorized, with explicit tests against look-alike hosts, userinfo, non-default ports, and non-https schemes.
  • Fail-closed wiring: isInterceptedHost extends the HTTPS-only/port-443-only enforcement to package hosts, so a package host cannot be reached in cleartext or on a non-standard port to bypass policy; credentialProblem refuses to start if a configured family lacks its credential or a package host lacks its TLS leaf.
  • New feed/view/project identifiers are validated newtypes (secure.rs::AdoFeedName) rather than raw strings, consistent with the repo convention, so they cannot carry path/shell metacharacters into generated policy or the agent-facing prompt text.
  • Header allow-lists (headers.ts) explicitly exclude authorization, x-nuget-apikey, npm-otp, and other credential-bearing headers from both directions.

No merge-blocking findings. Nothing in pr-review-comments.json to avoid duplicating (list was empty).

🔒 Security review by PR Security Reviewer · auto · 89.1 AIC · ⌖ 2.64 AIC · ⊞ 11.4K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Exceptionally thorough test suite for a security-sensitive feature (credential isolation, URL-shape authorization, redirect validation, header sanitization) — the adversarial-encoding and canary-leak tests in package.test.ts and proxy.e2e.test.ts are a model for this kind of work.

Two gaps worth a look, both advisory:

  1. validate_feed_exclusivity (new in src/runtimes/mod.rs, wired into all three runtime extensions) has no direct unit test for its own error paths — only the pre-migration codemod shape is tested, not the compiler-level rejection of runtimes.<x>.feed combined with feed-url/config.
  2. The "cuts off a streamed body" e2e test swallows the promise's outcome with .catch(() => undefined) and only asserts when it happens to resolve, with a bound loose enough to pass even if truncation silently regressed.

Nothing here blocks merge; the rest of the diff's test coverage (codemod round-tripping, policy cross-validation, per-protocol route tables, header allow/deny lists, audit-log scrubbing) is comprehensive and behavior-focused rather than implementation-mirroring.

🧪 Test quality analysis by Test Quality Sentinel · auto · 191 AIC · ⌖ 1.77 AIC · ⊞ 11K
Comment /review to run again

@github-actions github-actions Bot mentioned this pull request Oct 8, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rust Code Quality Review 🦀

Verdict: no merge-blocking issues found. This is a large (26-file) but carefully engineered addition of permissions.packages credential-isolated Azure Artifacts access. cargo check passes cleanly on the branch.

💡 Review notes
  • Error handling: consistent anyhow::bail!/.context() usage; no unwrap()/expect() outside #[cfg(test)] modules.
  • Correctness: percent-decoding in the codemod URL parser is bounds-checked (get(..) returns Option, not panicking slice indexing); feed-handle collision resolution (upsert_grant) is deterministic; Cargo's /Cargo/index/ URL casing is intentionally matched case-insensitively by the TS-side matchProtocol, not a bug.
  • Determinism: protocol lists are sorted/deduped before serialization (policy.rs), and audit stats use BTreeMap — consistent with the project's "generated YAML must be deterministic" rule.
  • Security-adjacent properties (credential custody, fail-closed wrapper routing) read as sound from the Rust side; the TS enforcement (package.ts, resolve-feeds.ts) is out of this reviewer's remit and is covered by the TypeScript and compiler-contract reviewers.
  • Test coverage is thorough: codemod migration/idempotency, cross-field validation (upstream: deny + Collaborator role rejection), runtime feed cross-referencing, and full pipeline-wiring assertions for both build-identity and WIF credential modes.

Process note: the rust-critic background sub-agent failed to start (400 model "gpt-5.4-mini" is not accessible), an infrastructure issue unrelated to this diff. This review reflects only my own direct pass over the Rust-only diff slice.

🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 190.7 AIC · ⌖ 1.94 AIC · ⊞ 11.3K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TypeScript Code Quality Review — scripts/ado-script/src/ado-proxy/

Note: the ts-critic sub-agent failed to start (model-access error: 400 model "gpt-5.4-mini" is not accessible via the /chat/completions endpoint), so this review reflects only my own direct pass, not a cross-check.

Scope reviewed

All new/changed TypeScript under scripts/ado-script/src/ado-proxy/ for the package-feed proxy feature: package.ts/.test.ts, config.ts/.test.ts, headers.ts/.test.ts, server.ts (handlePackage, openUpstream, streamBounded), resolve-feeds.ts/.test.ts, ca.ts, catalog.ts, index.ts.

Findings

No merge-blocking defects found.

  • Async/error handling: openUpstream/handlePackage/streamBounded correctly attach error handlers on every socket/stream before awaiting, back-pressure is honoured in streamBounded, and the single shared openUpstream helper avoids duplicated upstream-dialing logic between the REST and package paths. The finally { secured?.destroy() } cleanup is present on both handlers.
  • Credential isolation: token/packageTokens are now optional and read via readCredential(), each family's upstream only ever receives its own credential, and ca.ts's credentialProblem() fails closed at startup if a configured family lacks its credential or TLS leaf. Good design.
  • Type safety: the few as Segment/as string casts in package.ts::matchFeedRoute are all preceded by explicit length/undefined guards, so they're safe despite being non-null-assertion-adjacent. The caught as Error pattern in resolve-feeds.ts is pre-existing repo-wide idiom, not newly introduced risk.
  • Secret handling: resolve-feeds.ts redacts the token from every log line via redact() before escapeLoggingCommand(), and ca.ts fails closed on an empty-but-present credential field. I did spot what looked like a bare Authorization header / bearer-string leak in package.ts::packageAuthorizationHeader and resolve-feeds.ts's get() headers when viewing the files through tooling, but confirmed against git show HEAD:... that this is a sandbox/tool redaction artifact (the real source reads `Bearer ${token}`), not an actual code defect — flagging here only for transparency since it looked alarming at first glance.
  • Tests: every new branch (redirect allow/deny, 401/203 handling, response-too-large before and during streaming, packages-only policy denying REST, missing-credential paths, cleartext/CONNECT refusal for package hosts) has a matching proxy.e2e.test.ts / resolve-feeds.test.ts / package.test.ts case.

No inline comments posted — nothing met the bar for an actionable, line-specific defect.

🟦 TypeScript code quality review by TypeScript Code Quality Reviewer · auto · 172.1 AIC · ⌖ 6.61 AIC · ⊞ 11.3K
Comment /review to run again

jamesadevine added a commit that referenced this pull request Oct 9, 2026
…ctly

Review follow-ups on #2315:

- validate_feed_exclusivity had no direct test. Add unit tests for both
  conflicting combinations plus a per-runtime check through parse_markdown and
  each runtime extension.
- The "cuts off a streamed body" e2e test swallowed rejections and only
  asserted when the request resolved. It now accepts exactly two outcomes, a
  partial body bounded by the limit or a connection reset, and requires a new
  response-too-large decision record either way.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@jamesadevine

Copy link
Copy Markdown
Collaborator Author

/review

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

✅ TypeScript Code Quality Reviewer completed the TypeScript code quality review.

🟦 TypeScript code quality review by TypeScript Code Quality Reviewer

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: COMMENT. I found no merge-blocking Rust defects in the changed lines.

I read the Rust changes in the codemod, the package-feeds extension, runtimes/mod.rs and the compile/types changes. I did not start the rust-critic sub-agent. I also did not read every changed Rust file in full, so this is not an exhaustive review.

Notes
  • Error paths in the 0009 codemod carry the offending URL or key in their messages.
  • The remaining expect() calls in the codemod are guarded by earlier reads of the same keys, and the other unwrap() calls sit in tests.
  • Percent-decoding uses checked slicing, so malformed input returns None instead of panicking.
  • Advisory: when upsert_grant reuses an existing grant for the same feed, it does not apply the upstream: allow setting that a newly created grant gets. A runtime that previously relied on upstream saves would silently narrow. Consider documenting this or warning about it.

🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 35.5 AIC · ⊞ 11.3K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Security review: no end-to-end exploitable regressions found. I reviewed the core of ado-proxy/package.ts and did not read every one of the ~7.4k added lines.

  • The policy is deny-by-default: it fixes the package host, allows only GET/HEAD, and requires an exact feed, view and protocol grant.
  • Target parsing rejects double encoding, encoded slashes outside npm scoped names, ./.. segments, backslashes, control characters and fragments.
  • Redirects are never followed. They are relayed only to an https blob-storage suffix with an anchored label match, or to the same host after the route is re-authorized. Userinfo and non-default ports are refused.
  • The credential is attached only after the policy check.

Low-confidence hardening note: the redirect allowlist accepts any *.blob.core.windows.net account, not just the Azure Artifacts storage account. Only the upstream controls the Location, so I did not trace an exploit. Consider pinning to the account actually returned by the feed if feasible.

🔒 Security review by PR Security Reviewer · auto · 20.8 AIC · ⊞ 11.5K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Test quality review

Test coverage for this PR is strong overall:

  • TypeScript tests cover ca, catalog, catalog-drift, config, headers, package, resolve-feeds and an end-to-end proxy case.
  • Rust tests cover the new codemod and extension code.
  • tests/compiler_tests.rs adds a compile-level test.
  • The smoke cases are markdown plus a cases.json entry, which fits the lane model.
  • I found no deleted or ignored assertions. The dotnet test edits only swap the example host from pkgs.dev.azure.com to packages.example.test and still assert the same behaviour.

One advisory finding, posted inline: the bash-lint required-step list should include the new package-wrapper install step.

🧪 Test quality analysis by Test Quality Sentinel · auto · 20.9 AIC · ⊞ 11.1K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TypeScript review

The proxy changes look solid: the upstream sockets and requests have their late-error handlers, the credential is read after the allow decision, and redirects are never followed. One minor robustness finding is posted inline. The ts-critic sub-agent was not run in this pass; the findings above are from my own review.

🟦 TypeScript code quality review by TypeScript Code Quality Reviewer · auto · 44.2 AIC · ⊞ 11.3K
Comment /review to run again

@jamesadevine

Copy link
Copy Markdown
Collaborator Author

Thanks for the reviews. They're all addressed in 17d6868a:

  • TypeScript (resolve-feeds.ts stdin): applied. A stdin read failure now returns the structured ##vso[task.logissue] error with exit 1, and a test covers it.
  • Tests (bash-lint required steps): applied. Install package-manager wrappers (ado-proxy) is now in REQUIRED_STEP_DISPLAY_NAMES.
  • Rust (codemod reuses an existing grant): applied as a warning. Codemods must stay pure, so this adds a warnings channel to the framework. CodemodContext::warn records a note, CodemodReport::warnings carries it, and compile prints it. Codemod 0009 warns when it migrates a feed-url onto an author's view-less grant that doesn't set upstream: allow. If that grant is also invalid (upstream: deny, no view, Collaborator role), it already fails validation with the actionable error. Documented in docs/codemods.md.
  • Security (redirect not pinned to one blob account): declined.
    • The redirect target only comes from the authenticated Azure Artifacts response.
    • The proxy never follows it and never attaches the credential to it.
    • The client fetches the signed URL through Squid. The core AWF allowlist already admits *.blob.core.windows.net to the agent directly, so relaying the URL grants no new access.
    • Pinning to one account isn't possible because Artifacts shards its storage. In the live runs every download redirected to *.vsblob.vsassets.io.

@github-actions

This comment has been minimized.

…ns.packages

Agents can restore npm, PyPI, NuGet, and Cargo packages from Azure Artifacts
feeds without holding a feed credential. The trusted ado-proxy container holds
the build identity or a WIF-minted token and attaches it only to GET/HEAD
requests for granted feeds, views, and protocols.

- permissions.packages front matter (feeds, protocols, view, upstream,
  identity-role, optional service connection) and permissions-required.packages
- ado-proxy catalog v2 package family with a separate credential slot,
  host-side feed GUID resolution and access preflight, blob-only redirect relay
- per-process package-manager wrappers (npm, npx, pip, pip3, uv, dotnet, cargo)
- runtimes.<x>.feed source selection and runtimes.<x>.public-registry: block
- audit rollups by family/protocol and an upstream-unauthorized finding
- codemod 0009 migrates Azure Artifacts runtimes.<x>.feed-url

BREAKING CHANGE: Azure Artifacts runtimes.<x>.feed-url is replaced by
permissions.packages plus runtimes.<x>.feed. Codemod 0009 migrates existing
sources automatically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
jamesadevine and others added 7 commits October 16, 2026 21:43
…iable substitution

Run 647310 proved the build-identity npm path end to end: three package GETs
through ado-proxy, all allowed with 2xx upstream responses, including the
3.7 KB is-number tarball. npm then failed only because --pack-destination
must already exist and the agent has no mkdir. Write the tarball to the
working directory instead.

The WIF agent stopped before running any check: it compared its prompt with
the repository copy, saw $(Build.BuildId) replaced by a literal ID, and took
that for prompt injection. Both prompts now say up front that pipeline
variables are substituted and that repository files are not to be compared.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
Run 647315 proved npm end to end with both credentials (build identity and
WIF), but pip failed with SSLCertVerificationError against the proxy's leaf.
Recent pip builds its TLS context with truststore (the system trust store)
and PIP_CERT did not take effect; on Linux truststore reads OpenSSL's default
verify paths, which honour SSL_CERT_FILE. The pip wrapper now sets
SSL_CERT_FILE to the same system-roots-plus-proxy-CA bundle, still scoped to
the one process. PIP_CERT stays for pip's legacy-certs mode.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
…mpts

Run 647325 proved npm and PyPI end to end with the WIF credential, including
the npm tarball's 3xx relay to *.vsblob.vsassets.io. NuGet restore then failed
with NU1301: Squid logged NuGet's CONNECT to pkgs.dev.azure.com coming
straight from the agent, not from ado-proxy. NuGet takes its proxy from
http_proxy even for HTTPS feeds, and that variable still named Squid. The
dotnet wrapper now points http_proxy and HTTP_PROXY at the engine too. The
engine forwards other cleartext to Squid and refuses cleartext to intercepted
hosts.

The build-identity agent ran no checks: it found its prompt as a repository
file and decided the message was not its task. Both smoke prompts now open by
saying this is the task, that the repo copy is expected, and that the
repository is not to be inspected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
Run 647387 still saw NuGet reach pkgs.dev.azure.com straight from the agent:
dotnet never ran the package wrapper. AWF's entrypoint builds the agent PATH
from the host PATH (which carries our $GITHUB_PATH prepends) and then
prepends $DOTNET_ROOT, $CARGO_HOME/bin, $JAVA_HOME/bin, and others, so the
real dotnet shadowed the wrapper; cargo would have done the same. Re-assert
every awf_path_prepends directory at the front of PATH inside the engine
command, which runs after the entrypoint. Entries are compiler-owned and
character-checked because they are spliced into a single-quoted shell word.

The package-feed smokes also pass --no-custom-instructions: the ado-aw
repository's AGENTS.md otherwise loads as Copilot custom instructions and
cast the agent as a repository contributor, which made it decline the smoke
in several runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
…he registry

Run 647440 proved the whole package path with both credentials: npm, pip, and
NuGet restores through ado-proxy, the ungranted feed refused with 403, and no
credential in the sandbox (the WIF case passed every check). The one failure:
`public-registry: block` left registry.npmjs.org reachable, because the node
ecosystem also allows the bare apex npmjs.org and AWF matches subdomains.

Removal now drops any allowlist entry that would admit a blocked host under
AWF's subdomain matching, and the npm block list names the npmjs.org,
npmjs.com, and yarnpkg.com apexes explicitly. A new test compiles all three
runtimes blocked (plus covering network.allowed entries) and asserts no
remaining entry admits any blocked host.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
Full candidate run 647482 passed every case, including package-feeds
(build identity) and package-feeds-wif, so the "needs live validation"
limitation is replaced by a description of what the smoke suite proves.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
- resolve-feeds: a stdin read failure now returns the structured
  ##vso[task.logissue] error (exit 1) instead of rejecting with a raw stack
  trace; covered by a test.
- bash lint: "Install package-manager wrappers (ado-proxy)" joins
  REQUIRED_STEP_DISPLAY_NAMES, so losing that fixture coverage fails the test.
- codemods: add a warnings channel. Codemods stay pure: CodemodContext::warn
  records a note, CodemodReport::warnings carries it, and compile prints it
  under the "applied codemods" list.
- codemod 0009 uses it when it migrates a feed-url onto an author's existing
  view-less grant that does not set `upstream: allow`, since the agent can no
  longer pull new upstream packages through it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
@jamesadevine
jamesadevine force-pushed the feat/package-feed-proxy branch from 17d6868 to 45c3ca4 Compare October 16, 2026 20:44
@jamesadevine
jamesadevine changed the base branch from fix/runtime-feed-credential-isolation to main October 16, 2026 20:44
@github-actions

This comment has been minimized.

…ge config

The package proxy only intercepts pkgs.dev.azure.com, so a committed
nuget.config, .npmrc, pip.conf, uv.toml, pyproject.toml, .yarnrc.yml, or
.cargo/config(.toml) that still names {org}.pkgs.visualstudio.com sends
restores to Azure Artifacts with no credential: they fail with 401 and
nothing says why. Codemod 0009 already rewrites front matter; this covers the
files the compiler does not own.

A new warning-only check scans those files at the source repository's git
root, finds legacy-host URLs (including npm's protocol-relative //host keys),
and suggests the canonical pkgs.dev.azure.com/{org}/... URL. It runs only
when the workflow can restore packages (permissions.packages or a
node/python/dotnet runtime). compile prints it, and `ado-aw lint` and the
lint_workflow MCP tool report it as `legacy-feed-url` warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
@github-actions

This comment has been minimized.

…fuse

Extends the committed package-config check: besides legacy-host URLs, it now
reports pkgs.dev.azure.com feed URLs in nuget.config, .npmrc, pip.conf, and
the rest that permissions.packages does not cover. The match mirrors the
proxy's authorizePackageRequest. Each finding names its reason:
feed-not-granted (organization, project, feed; an organization-scoped grant
never covers a project-scoped URL), view-not-granted, protocol-not-granted,
unknown-route, or no permissions.packages at all.

It never reports a URL the proxy would allow. GUID identifiers and grants
without organization cannot be decided before runtime resolution, so they are
treated as matching. compile prints the warnings, and `ado-aw lint` and the
lint_workflow MCP tool report them as `ungranted-feed-url`.

Also fixes the AGENTS.md entry from the previous commit, where escaping had
turned the backticks around `legacy-feed-url` into backslashes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b0ab0d4-23f4-4a89-90ea-3c40758ea550
@github-actions

This comment has been minimized.

@jamesadevine

Copy link
Copy Markdown
Collaborator Author

/review

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

✅ Rust Code Quality Reviewer completed the Rust code quality review.

🦀 Rust code quality review by Rust Code Quality Reviewer

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Compiler-contract review: no drift found. Codemod 0009 is registered, the PackageFeeds extension is wired into collect_extensions(), the ado-proxy catalog generated files moved with their source, and docs and AGENTS.md were updated. The new smoke cases don't add committed lock files, per-case definition IDs or format!-built shell. I did not check .gen.ts or .gen.json content against the Rust sources. Run npm run codegen in scripts/ado-script to confirm nothing drifts.

🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 21.5 AIC · ⊞ 12.6K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TypeScript review of scripts/ado-script/ found no merge-blocking defects. I checked the ado-proxy changes (resolve-feeds.ts, index.ts, server.ts) for async and error handling, secret leakage and external-input validation.

  • Credential handling: In resolve-feeds.ts the token comes from stdin only, redirects are set to manual, and every message passes through redact. Logging-command output is escaped.
  • Network calls: Requests have a timeout and abort handling.
  • Advisory, unchanged code: void main() in ado-proxy/index.ts has no .catch, so a throw from run() would be an unhandled rejection. This line is not part of the diff, so no inline comment was possible.
  • Advisory, minor: (caught as Error).message casts in resolve-feeds.ts assume the thrown value is an Error. Narrowing with caught instanceof Error would be safer.
  • Sub-agent: I did not start ts-critic, so this review is my own pass only.

🟦 TypeScript code quality review by TypeScript Code Quality Reviewer · auto · 28.5 AIC · ⊞ 11.4K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Test quality review

Overall coverage is strong (TS unit/e2e for package.ts and resolve-feeds, codemod, audit, compile-level wiring, bash-lint fixture registered, smokes as markdown + cases.json). No assertions were weakened; the removed ones were split into stricter cases or retargeted to a neutral host. Advisory gaps are in the inline comments: cargo/npm wrapper branches unasserted and one vacuous || assertion.

🧪 Test quality analysis by Test Quality Sentinel · auto · 46.6 AIC · ⊞ 11.1K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Security review: no regressions found. The package-feed family is deny-by-default with GET/HEAD only. The credential is injected only after the policy decision and the client's own credential headers are stripped. Redirects are never followed, only relayed to blob-storage hosts or granted same-host routes. The wrapper and CA scripts use quoted bindings and carry no token (the Cargo helper prints a sentinel). The package token reaches only the trusted proxy step as a secret env var, not the Agent. Path parsing refuses traversal, encoded separators and double encoding. I read the diff, not the running code, and did not run the tests.

🔒 Security review by PR Security Reviewer · auto · 30.6 AIC · ⊞ 11.5K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: COMMENT — no merge-blocking Rust defects found.

Two advisory findings on the duplicated percent_decode helpers.

Themes
  • Percent-decoding accepts signed hex (%+a); duplicated in two files.
  • Not posted (low value / unverified line mapping): blocking std::fs reads in async compile path, swallowed read_dir error and symlink-following in the config scan.

🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 31.5 AIC · ⊞ 11.3K
Comment /review to run again

Use shared strict percent-decoding so malformed package feed URL escapes are rejected consistently. Strengthen package-wrapper and service-connection regression assertions to cover the corrected behavior.

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

Copilot-Session: 2118822c-f81d-4b93-a8a9-662fca1d23fb
@github-actions

Copy link
Copy Markdown
Contributor

Prompt evaluation

Note

This is an advisory static review. Only Prompt Contracts is merge-blocking.

Prompt Cases Improved Unchanged Regressed Inconclusive
create 3 0 3 0 0
update 0 (not selected) - - - -
debug 0 (not selected) - - - -

The only change to prompts/create-ado-agentic-workflow.md adds one reference URL (docs/package-feeds.md) to the documentation list. It changes no instructions, confirmation policy, safe-output guidance or compile/lint steps. Every rubric criterion should therefore score the same as base for the three create cases.

Potential regressions

None identified.

Per-case scores
Case Prompt Base Candidate Result
create-minimal-manual create same same unchanged
create-needs-clarification create same same unchanged
create-scheduled-workitem-report create same same unchanged

Absolute per-criterion scores were not computed because the diff is a single added link, so base and candidate score the same on every criterion.

Generated by Prompt Evaluator for #2315 · auto · 22.9 AIC · ⊞ 6.4K · ◷

@jamesadevine
jamesadevine merged commit 036174c into main Oct 27, 2026
34 of 35 checks passed
@jamesadevine
jamesadevine deleted the feat/package-feed-proxy branch October 27, 2026 19:00
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.

1 participant