feat(mcp): refresh Azure identity for long-running tasks - #2097
feat(mcp): refresh Azure identity for long-running tasks#2097jamesadevine wants to merge 20 commits into
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Refresh cached Azure DevOps clients when short-lived tokens rotate and add direct coverage for default auth, PR repository selectors, and branch/tag E2E scenario behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Verify a failed required cross-organization target is retained while later repository preparation continues. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Use a closed token-variable enum, restrict repository identity folding to ASCII, update proxy credential docs, and complete failure-path coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Pin the complete repos entry emitted alongside expanded write permissions in executor E2E sources. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Assert repository target names with more than project/repository segments fail closed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c8e5f36-958b-48ea-99cd-d7c14b5eb8da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d6d08ba7-33cd-4c10-8c9e-69f19c8e3132
|
Azure Pipelines: Successfully started running 1 pipeline(s). 1 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
/review |
|
✅ Rust Code Quality Reviewer completed the Rust code quality review.
|
There was a problem hiding this comment.
TypeScript review — scripts/ado-script/src/azure-wif-refresh/
Reviewed the new sidecar script (773 lines) plus its test suite. ts-critic ran in the background but returned no findings; my own pass agrees there is nothing merge-blocking here.
Highlights that make this solid:
systemAccessToken/initialIdTokennever reach a log line or thrownError— allreport(...)calls use static, sanitized strings.- Network calls go through
requestWithTimeoutwith anAbortSignalrace plus a hard timeout, and retries back off correctly against the assertion's actual expiry. writeAtomicuses a same-directory temp file +rename, with cleanup of the handle and temp file on every failure path.parseMaterialallow-lists fields and validates every value's shape (including a GUID regex forserviceConnectionId) before anything touches the state machine.
One minor, non-blocking nit worth a look before merge: in onData (readOneJsonDocument), Buffer.byteLength(buffer) is recomputed against the entire accumulated buffer on every chunk to enforce MAX_MATERIAL_BYTES, which is O(n2) for many small chunks. Given input is bounded to 1MB from a trusted host, this is a performance nit rather than a defect — track a running byte count instead if this ever gets exercised with high chunk counts.
No inline comments needed.
🟦 TypeScript code quality review by TypeScript Code Quality Reviewer · auto · 40.9 AIC · ⌖ 10.5 AIC · ⊞ 11.6K
Comment /review to run again
There was a problem hiding this comment.
Security review — no regressions found
Reviewed the new Azure workload-identity refresh sidecar (azure-wif-refresh.js), its Rust wiring (agentic_pipeline.rs, mcpg.rs, common.rs, ado_bundle.rs, azure_cli.rs, types.rs, secure.rs), and doc updates.
This is a well-defended addition to the credential-isolation model, not a weakening of it:
System.AccessTokenand the initialidTokenreach the sidecar only via a one-shot, mode-600 FIFO (mkfifo -m 600), never as a container env var, CLI argument, or file — confirmed in both the generated bash (START_AZURE_WIF_REFRESH) and the TypeScriptparseMaterial/writeAtomicpaths.- The refresher container is hardened (
--cap-drop ALL,--security-opt no-new-privileges,--read-only, non-root--user,--pids-limit 64) and only the token-only subdirectory is mounted read-only into the target MCP container — the sidecar's own status/ready/material channels stay outside that mount. - New
ContainerAbsolutePathvalidator (secure.rs) rejects traversal,:mount separators, shell metacharacters, and even##vso[injection inmount-path— good defense-in-depth for a value ultimately used in a Docker mount flag. common.rsvalidation blocks userargs/env/mountsfrom colliding with or overriding the compiler-ownedAZURE_CLIENT_ID/AZURE_TENANT_ID/AZURE_FEDERATED_TOKEN_FILEenv vars or the auth mount path, andazure-authis correctly restricted to containerized stdio servers only (rejected forsafeoutputs, extension-owned servers, and HTTP servers).mcpg.rs's newbind_internal_pipeline_variable/required_namesmachinery keeps the compiler-internal client/tenant-id variables out of user-writableMCPG_ENV_NAMESwhile still enforcing they're non-empty and not an unresolved ADO macro ($() before MCPG starts — a good fail-closed check against silent misconfiguration.- The sidecar's
report()logging only emitsErrorCategorystrings, never token/material content, anderrorCategory()derives categories from HTTP status/error codes without echoing response bodies. - Cleanup (
STOP_AZURE_WIF_REFRESH) runs unconditionally (Condition::Always) and removes the private auth directory even on partial-startup failure.
No injection, token-scope, or path-traversal issues found in this diff. Nice attention to the /tmp chroot-exposure trap called out in the docs update — the credential directory correctly stays under a private subpath of Agent.TempDirectory, distinct from the shared runner /tmp that AWF exposes to the agent.
🔒 Security review by PR Security Reviewer · auto · 67.1 AIC · ⌖ 2.03 AIC · ⊞ 11.7K
Comment /review to run again
There was a problem hiding this comment.
Overall the test coverage for this PR is strong — the mcpg.rs rejection paths (HTTP servers, env/mount collisions, unsafe mount paths) and the runRefresher state-machine tests are thorough and behavior-focused rather than implementation-mirroring.
Three gaps worth closing before merge:
readOneJsonDocument(chunked stdin JSON parser with brace-depth/string-escape tracking) has zero direct test coverage.errorCategoryclassifies 7 categories but onlythrottled/invalid-response/generic-server paths are exercised indirectly throughrunRefresher—timeout,client,network, andunknownare untested.- The new
ContainerAbsolutePathvalidator's security-critical checks (##vso[injection guard, control characters\0/\n/\r) aren't asserted incontainer_absolute_path_rules, only path-traversal and colon cases are.
None of these are blocking — the core refresh/rotation logic is well tested — but they're the highest-value additions if there's time before merge.
🧪 Test quality analysis by Test Quality Sentinel · auto · 67.8 AIC · ⌖ 2.47 AIC · ⊞ 11.3K
Comment /review to run again
There was a problem hiding this comment.
Rust review — no merge-blocking defects found
Reviewed the Rust portions of this PR (src/compile/ado_bundle.rs, agentic_pipeline.rs, common.rs, extensions/ado_script.rs, extensions/mod.rs, ir/tasks/azure_cli.rs, mcpg.rs, types.rs, secure.rs, tests/compiler_tests.rs). Note: the rust-critic sub-agent I launched did not receive the diff content due to a shell-substitution issue on my side, so this review reflects only my own manual pass — flagging for transparency rather than treating it as a clean bill from two independent reviewers.
Positives:
McpgLaunchEnvironment::bindingsis aBTreeMap<McpgEnvName, _>, sonames()/required_names()iteration order is deterministic — important since it feeds generated YAML env lists.- The
ContainerAbsolutePathvalidator correctly rejects traversal,:/$/backtick, and empty/./..segments before the path is ever used to build a Docker mount destination. - Reserved-env and mount-collision checks in
validate_stdio_mcpcorrectly bail beforeazure-authcredentials could be shadowed or a container mount could collide with the token mount. SYSTEM_ACCESSTOKENis injected viaEnvValue::secret("System.AccessToken")rather than a literal, keeping it out of the rendered YAML/log surface, consistent with the project's credential-isolation model.
One non-blocking hardening note:
The Docker-flag detector in validate_stdio_mcp (common.rs ~L3269-3277) matches only unbundled short/long flags (-e, --env, -v, --volume, --mount, --volumes-from, and their =value forms) plus the -eX/-vX bundled-value case. It does not reject a bundled multi-short-flag form such as -itve where -e/-v is not the first character. I could not confirm whether the downstream runtime.extra_args consumer parses args in a way that would treat this as equivalent to -e/-v (most likely it passes them straight through to docker run's own arg vector, which would not be vulnerable), so this is flagged only for defense-in-depth review since it's a security-relevant guard, not because I have a working bypass.
Nothing else in the diff rises to merge-blocking: error handling uses anyhow with context consistently, no new unwrap/expect on user-reachable paths, and the new typed AzureCliV3/ContainerAbsolutePath/McpgLaunchEnvironment additions all follow existing patterns in this codebase.
🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 98.1 AIC · ⌖ 12.7 AIC · ⊞ 11.6K
Comment /review to run again
Summary
mcp-servers.<name>.azure-authfor ARM workload-identity service connectionsSystem.OidcRequestUriSystem.AccessTokenand refresh credentials out of the agent, MCP environment, Docker arguments, logs, and artifactsCloses #1964.
Test plan
cargo testcargo clippy --all-targets --all-features -- -D warningscargo test --test generated_shell_guardcargo test --test bash_lint_testsnpm testinscripts/ado-scriptnpm run typecheckinscripts/ado-scriptnpm run build:azure-wif-refreshinscripts/ado-scripttests/azure-wif-refresh-e2e/azure-pipelines.ymlThe credentialed Azure-native E2E definition is included, but its live run requires an authorized ARM workload-identity service connection.