Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2b02ec8
feat(permissions): add typed repository write configuration
jamesadevine Sep 2, 2026
23954ac
chore(compile): preserve agentic pipeline formatting
jamesadevine Sep 2, 2026
286ef7f
refactor(compile): move ADO token acquisition to AzureCLI@3
jamesadevine Sep 2, 2026
5bfada3
refactor(safe-outputs): resolve repository write targets centrally
jamesadevine Sep 2, 2026
c4bc7de
feat(safe-outputs): support cross-org branch and tag writes
jamesadevine Sep 2, 2026
09b87e1
feat(safe-outputs): support cross-org pull requests
jamesadevine Sep 2, 2026
d18c1cd
fix(safe-outputs): harden repository target routing
jamesadevine Sep 2, 2026
6c12a3a
feat(compile): prepare cross-org pull-request bases securely
jamesadevine Sep 2, 2026
180f90e
fix(compile): report cross-org write readiness
jamesadevine Sep 2, 2026
9c2cc3b
fix(safe-outputs): enforce cross-org auth and dry-run parity
jamesadevine Sep 2, 2026
059eb79
test(safe-outputs): cover cross-org repository writes
jamesadevine Sep 2, 2026
77cfe9d
docs: document cross-org repository writes
jamesadevine Sep 2, 2026
0732b19
Merge remote-tracking branch 'origin/main' into feat/cross-org-reposi…
jamesadevine Sep 2, 2026
576cba5
test(safe-outputs): address cross-org review gaps
jamesadevine Sep 2, 2026
4cffec4
test(ado-script): cover multi-repository preparation failure
jamesadevine Sep 2, 2026
612b6ea
fix(compile): close cross-org review findings
jamesadevine Sep 2, 2026
7e9f02f
test(ado-script): assert cross-org repository rendering
jamesadevine Sep 2, 2026
57452e6
test(safe-outputs): cover invalid repository target shape
jamesadevine Sep 2, 2026
d2a6e88
Merge remote-tracking branch 'origin/main' into feat/cross-org-reposi…
jamesadevine Sep 3, 2026
4f12230
feat(mcp): refresh Azure identity for long-running tasks
jamesadevine Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ fail-closed and only pauses when the agent actually proposed a reviewed output.
│ ├── executor-e2e/ # Stage 3 safe-output E2E test harness (not a bundle; runs deterministic scenarios against a real ADO project and files a GitHub issue on failure)
│ ├── compiler-smoke-e2e/ # Smoke E2E orchestrator (not a bundle): stages each case in `tests/smoke/cases.json` to the fixed `.smoke/pipeline.yml` path on its own per-case `ado-aw-mirror` ref, queues it against its credential *lane* definition, and asserts they go green. Two modes via `SMOKE_COMPILER_SOURCE`: `candidate` (compiler built from this commit, pinned pipeline-artifact) and `released` (latest release asset, release URLs required). Built to `test-bin/` by `build:compiler-smoke-e2e`, listed in `NON_BUNDLE_DIRS`.
│ ├── prepare-pr-base/ # create-pull-request preparer (bundled to prepare-pr-base.js): Agent mode uses ADO diff metadata + bounded fallback; SafeOutputs fetches the target tip; cross-org targets use isolated credentials + exact remote matching
│ ├── azure-wif-refresh/ # Renewable Azure Pipelines workload-identity assertion writer for user-defined stdio MCP servers; trusted sidecar receives request credentials on stdin and rotates a private token file
│ ├── ado-proxy/ # Credential-isolated ADO policy proxy (bundled to ado-proxy.js). The pipeline mounts it into node:20-slim and starts it before AWF; AWF attaches the trusted container via --topology-attach. scope.ts builds the organization-relative current/additional scope index; catalog.gen.json + ../shared/ado-proxy-catalog.types.gen.ts are generated from Rust by export-ado-proxy-catalog{,-schema} and drift-guarded; a catalog_version mismatch fails closed at startup.
│ ├── trigger-e2e/ # Test-only gate-spec / trigger-evaluation harness (not a bundle): mirrors Rust `Fact::ALL` in `gate-spec.ts`; `fact-catalog.gen.json` is generated by `export-fact-catalog` and drift-guarded by CI
│ └── shared/ # Shared modules across bundles (auth, ado-client, env-facts, types.gen.ts)
Expand Down Expand Up @@ -459,7 +460,8 @@ index to jump to the right page.
(`scripts/ado-script/`): the bundled TypeScript runtime helpers
(`gate.js`, `import.js`, the execution-context `exec-context-*.js`
bundles, `conclusion.js`, `approval-summary.js`,
`github-app-token.js`, and `prepare-pr-base.js`), schemars-driven
`github-app-token.js`, `prepare-pr-base.js`, and
`azure-wif-refresh.js`), schemars-driven
type codegen, the A2 design decision, the bundle env contract
modelled in `src/compile/ado_bundle.rs`, and the `trigger-e2e/`
gate-spec drift guard (kept in sync via `export-fact-catalog`).
Expand Down
12 changes: 11 additions & 1 deletion docs/ado-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
`ado-script` is the umbrella name for the TypeScript workspace at
[`scripts/ado-script/`](../scripts/ado-script/). It produces small,
ncc-bundled Node programs that the **compiler injects into every emitted
pipeline** as runtime helpers. Today it produces thirteen bundles:
pipeline** as runtime helpers. Today it produces the following shipped bundles:

- `gate.js` — trigger-filter gate evaluator (Setup job).
- `import.js` — runtime prompt resolver described in
Expand Down Expand Up @@ -88,6 +88,16 @@ pipeline** as runtime helpers. Today it produces thirteen bundles:
shell-local or in masked `SYSTEM_ACCESSTOKEN` env and spawned-git
`GIT_CONFIG_*`, never argv or `.git/config`. Runs outside AWF. See
[`safe-outputs.md`](safe-outputs.md#create-pull-request).
- `azure-wif-refresh.js` — long-lived trusted sidecar for
`mcp-servers.<name>.azure-auth`. It receives the initial Azure Pipelines
workload-identity assertion and `System.AccessToken` in a one-shot stdin
document, requests replacement assertions from `System.OidcRequestUri` using
the runtime service-connection GUID, and atomically rotates a mode-0644 file
inside a private mode-0700 host directory mounted read-only into
the target MCP container. Request credentials remain in sidecar memory and
never enter the agent, MCP environment, Docker arguments, logs, status
documents, or artifacts. See
[`mcp.md`](mcp.md#renewable-azure-workload-identity).

> **Internal-only.** `ado-script` is not a user-facing front-matter
> feature. Authors never write an `ado-script:` block in their agent
Expand Down
3 changes: 3 additions & 0 deletions docs/front-matter.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ mcp-servers:
CUSTOM_TOKEN:
pipeline-variable: CUSTOM_TOKEN # ADO pipeline/variable-group/same-job source
STATIC_CONFIG: "value" # literal value embedded in MCPG config
azure-auth: # optional renewable Azure workload identity
service-connection: my-arm-service-connection
mount-path: /var/run/ado-aw/azure # optional; token is written below this path
allowed:
- custom_function_1
- custom_function_2
Expand Down
62 changes: 62 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ mcp-servers:
- `env:` - Environment variables for the MCP server process. Use a string for
a static value or `{ pipeline-variable: NAME }` to read an ADO pipeline,
variable-group, queue-time, or earlier-same-job variable at runtime.
- `azure-auth:` - Renewable Azure workload identity from an ARM service
connection. The compiler supplies the Azure Identity environment contract
and rotates the federated assertion for the lifetime of the Agent job.
Supported only for containerized stdio servers.
- `service-connection:` - Required ARM workload-identity service connection.
- `mount-path:` - Optional container directory for the assertion; defaults
to `/var/run/ado-aw/azure`.

**HTTP servers:**
- `url:` - HTTP endpoint URL for the remote MCP server
Expand Down Expand Up @@ -83,6 +90,56 @@ variable-group, and queue-time variables exist from job start; a
`task.setvariable` source must be published by an earlier step in the same job.
Cross-job/stage output expressions are not accepted by `pipeline-variable`.

## Renewable Azure workload identity

Use `azure-auth` when a containerized MCP server uses an Azure Identity SDK and
may need to acquire an Azure token late in a long-running Agent job:

```yaml
mcp-servers:
kusto:
container: "node:22-slim"
entrypoint: "sh"
entrypoint-args:
- "-c"
- "exec npx -y @azure/mcp@latest server start --namespace kusto"
azure-auth:
service-connection: my-arm-service-connection
# Optional; defaults to /var/run/ado-aw/azure
mount-path: /var/run/ado-aw/azure
```

The compiler injects these values into the MCP container:

```text
AZURE_CLIENT_ID=<service-connection client ID>
AZURE_TENANT_ID=<service-connection tenant ID>
AZURE_FEDERATED_TOKEN_FILE=/var/run/ado-aw/azure/token
```

An AzureCLI@3 setup task obtains the initial workload-identity assertion and
starts a trusted refresh sidecar. The sidecar uses the job's
`System.AccessToken` and `System.OidcRequestUri` to request replacement
assertions before their JWT expiry and atomically rotates the private token
file. The MCP container receives the token directory through a read-only
mount, so inode-replacing rotation remains visible; the agent and MCP server never receive
`System.AccessToken`.

`azure-auth` fails closed when:

- the server is HTTP or has no `container`;
- the service connection does not use workload identity federation;
- the author also sets `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, or
`AZURE_FEDERATED_TOKEN_FILE`;
- a user mount overlaps the compiler-owned destination;
- the initial assertion or refresher readiness check fails.

The assertion is not an Azure access token. Azure Identity inside the MCP
container exchanges it for the resource-specific access token requested by the
server. AzureCLI@3's experimental `keepAzSessionActive` option does not replace
this feature: it refreshes only while that AzureCLI task remains running, but
the MCP server is used later during the separate Agent step.

The first-party `tools.azure-devops` integration is deliberately different:
it gives the MCP a non-secret sentinel in `ADO_MCP_AUTH_TOKEN`. The real
`SC_READ_TOKEN` is delivered only to `ado-proxy` over stdin and is injected
Expand Down Expand Up @@ -119,3 +176,8 @@ network:
4. **MCPG Gateway**: All MCP traffic flows through the MCP Gateway which enforces tool-level filtering
5. **Trusted egress**: MCPG and the stdio/HTTP backends it spawns from `mcp-servers:` front matter are trusted infrastructure that runs outside the agent's Squid-enforced allowlist — they have direct network egress and are not subject to `network.allowed`/`network.blocked`. Only the Copilot agent process itself is confined to the AWF sandbox and its domain allowlist; see [`docs/mcpg.md`](mcpg.md) and [`docs/network.md`](network.md) for the topology.
6. **SafeOutputs is further hardened**: unlike arbitrary `mcp-servers:` entries, the compiler-owned `safeoutputs` MCPG backend is not a user-configurable trusted-egress container — it is a dedicated stdio child spawned by MCPG from the pinned AWF `agent` image with `--network none`, `--cap-drop ALL`, a read-only rootfs, and the host ADO runner's non-root UID/GID. It has no network access at all, trusted or otherwise; see [`docs/mcpg.md`](mcpg.md).
7. **Azure credential custody**: `azure-auth` keeps `System.AccessToken` in the
trusted AzureCLI@3/refresher path. Only the short-lived federated assertion
is mounted into the target MCP, read-only. Credential files are created
beneath `$(Agent.TempDirectory)`, never runner `/tmp`, because AWF exposes
runner `/tmp` inside the agent sandbox.
12 changes: 10 additions & 2 deletions docs/mcpg.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ no bridge-gateway resolution, and no `host.docker.internal` mapping.
internal request through Squid.
5. MCPG routes tool calls to the appropriate upstream (SafeOutputs or custom
MCPs). Detection is unaffected — it never attaches to MCPG.
6. After the agent completes, MCPG (and any stdio children it spawned,
6. For a custom stdio MCP with `azure-auth`, a separate trusted
`azure-wif-refresh.js` sidecar rotates a federated assertion beneath
`$(Agent.TempDirectory)`. MCPG mounts only its token directory read-only into
the target MCP container and forwards non-secret client/tenant IDs through
its typed launch environment.
7. After the agent completes, MCPG (and any stdio children it spawned,
including SafeOutputs) are stopped.

## MCPG Configuration Format
Expand Down Expand Up @@ -163,6 +168,9 @@ The MCPG is automatically configured in generated standalone pipelines:
1. **Config Generation**: The compiler generates `mcpg-config.json` from the agent's `mcp-servers:` front matter, including the compiler-owned `safeoutputs` stdio entry above.
2. **MCPG Start**: The MCPG Docker container (`awmg-mcpg`) starts on Docker's bridge network, published to the host at `127.0.0.1:8080`, with config via stdin and the Docker socket mounted so it can spawn stdio children (including SafeOutputs) on demand.
3. **Agent Execution**: AWF runs the Agent rootlessly with `--network-isolation --topology-attach awmg-mcpg`, attaching the MCPG container to `awf-net`; copilot connects to MCPG at `awmg-mcpg:8080` over HTTP, and reaches SafeOutputs tools transparently through MCPG's stdio routing.
4. **Cleanup**: MCPG and any stdio children it spawned (including SafeOutputs) are stopped after the agent completes (condition: always).
4. **Cleanup**: MCPG and any stdio children it spawned (including SafeOutputs)
are stopped after the agent completes (condition: always). Renewable Azure
assertion sidecars are then stopped and their private
`$(Agent.TempDirectory)/ado-aw-azure-auth/` directories removed.

The MCPG config is written to `$(Agent.TempDirectory)/staging/mcpg-config.json` in its own pipeline step, making it easy to inspect and debug. SafeOutputs is always run with the `ado-aw mcp` stdio subcommand through MCPG.
23 changes: 23 additions & 0 deletions docs/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@ not found" failure mode.
See [`docs/tools.md`](tools.md#built-in-clis) for the agent-facing
contract (auth scope, available subcommands).

## Renewable Azure authentication for MCP servers

`mcp-servers.<name>.azure-auth` is a trusted-infrastructure credential path for
containerized stdio MCP servers. It is separate from the agent-facing Azure CLI
wrapper described above:

- AzureCLI@3 receives an ARM workload-identity service connection and exposes
the initial federated assertion only to its trusted setup script.
- `System.AccessToken` and the initial assertion are streamed to a dedicated
refresh sidecar over a one-shot FIFO; neither is stored in Docker
environment, command arguments, generated YAML, or a host credential file.
- The sidecar keeps the ADO request credential in memory and writes only the
renewable federated assertion beneath `$(Agent.TempDirectory)`.
- MCPG mounts the assertion's token-only directory read-only into the configured
MCP container, so atomic file replacement is visible without exposing
sidecar status or material channels.
- The AWF agent receives no credential mount, no identity environment
variables, and no route to the refresher container.

The credential directory must not move to runner `/tmp`: AWF mounts runner
`/tmp` into the agent chroot, making files there agent-readable. See
[`docs/mcp.md`](mcp.md#renewable-azure-workload-identity) for configuration.

## Adding Additional Hosts

Agents can specify additional allowed hosts in their front matter using either ecosystem identifiers or raw domain patterns:
Expand Down
1 change: 1 addition & 0 deletions scripts/ado-script/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ conclusion.js
github-app-token.js
prepare-pr-base.js
ado-proxy.js
azure-wif-refresh.js
schema
*.tsbuildinfo
test-bin
Loading
Loading