Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b0cb523
fix(compile): correct az devops authentication claims in prompt and docs
jamesadevine Jul 31, 2026
cdc2316
feat(ado-proxy): add credential-isolated Azure DevOps policy proxy
jamesadevine Jul 31, 2026
87516f9
test(ado-script): budget for Vite transform time in the test timeout
jamesadevine Jul 31, 2026
61f5acc
fix(compile): guard the front-matter capability enum against the prox…
jamesadevine Jul 31, 2026
8f99b90
docs(ado-proxy): revise design for per-client ingress; add SPS discov…
jamesadevine Aug 1, 2026
e80596f
test(ado-proxy): prove --add-host redirection and that SPS is avoidable
jamesadevine Aug 1, 2026
2045f05
fix(ado-proxy): rewrite resource-area locationUrl instead of filterin…
jamesadevine Aug 1, 2026
f78b2b3
refactor(ado-proxy): read interception certificates from stdin
jamesadevine Aug 2, 2026
13e1535
feat(ado-proxy): accept direct TLS alongside the CONNECT path
jamesadevine Aug 2, 2026
622dac0
docs(ado-proxy): mount the MCP package; record the credential-deliver…
jamesadevine Aug 2, 2026
db3282b
feat(ado-proxy): deliver the bearer on stdin and bound the run to its…
jamesadevine Aug 2, 2026
e4cd6c9
refactor(ado-proxy): carry interception material as versioned JSON
jamesadevine Aug 2, 2026
1ea098c
feat(ado-proxy): add the policy-engine container lifecycle
jamesadevine Aug 2, 2026
bcdf79f
feat(ado-proxy): attach the policy engine to the AWF network
jamesadevine Aug 3, 2026
cafb187
feat(ado-proxy): route the Azure DevOps MCP through the policy engine
jamesadevine Aug 3, 2026
3a91f3e
fix(ado-proxy): isolate the MCP network so it cannot route past the e…
jamesadevine Aug 3, 2026
156ea00
feat(ado-proxy): add the az wrapper and fix the CA for strict verifiers
jamesadevine Aug 3, 2026
8019fd8
feat(ado-proxy): install the az wrapper into the agent sandbox
jamesadevine Aug 3, 2026
db31f8c
feat(ado-proxy): tell the agent what az can actually do, and allow az…
jamesadevine Aug 3, 2026
2634dfa
fix(ado-proxy): populate the policy from config, restoring repository…
jamesadevine Aug 4, 2026
a241805
feat(ado-proxy): resolve scope through an organization-relative index
jamesadevine Aug 4, 2026
4e4987a
test(ado-proxy): prove organization-relative scope resolution
jamesadevine Aug 4, 2026
d7e4bc3
feat(ado-proxy): emit explicit cross-organization read scopes
jamesadevine Aug 4, 2026
4fbf8ae
feat(ado-proxy): accept structured permissions.read policies
jamesadevine Aug 4, 2026
45995cf
fix(ado-proxy): require a read token source when proxying ADO
jamesadevine Aug 4, 2026
ff3d613
feat(ado-proxy): derive repository-only scopes from repos
jamesadevine Aug 4, 2026
157c14b
test(ado-proxy): prove scoped reads across compiler and bundle
jamesadevine Aug 4, 2026
ed4c14e
docs(ado-proxy): document scoped credential-isolated reads
jamesadevine Aug 4, 2026
c90e7a9
feat(ado-proxy): enable the scoped proxy runtime
jamesadevine Aug 4, 2026
c527ff2
docs(ado-proxy): finalize shipped behavior and contributor guidance
jamesadevine Aug 5, 2026
fafef14
Merge remote-tracking branch 'origin/main' into devinejames/mcpg-az
jamesadevine Aug 5, 2026
b548b79
test(ado-proxy): add candidate runner smoke coverage
jamesadevine Aug 5, 2026
3ee4442
fix(ado-proxy): detach the proxy lifecycle and publish diagnostics
jamesadevine Aug 5, 2026
1f31bb9
fix(ado-proxy): publish the interception CA as world-readable
jamesadevine Aug 5, 2026
4f14cca
fix(ado-proxy): accept the MCP project-list query shape
jamesadevine Aug 5, 2026
92239f2
fix(ado-proxy): give wrapped az a writable config directory
jamesadevine Aug 5, 2026
507a7a8
test(ado-proxy): exercise runtime denials on the ADO runner
jamesadevine Aug 5, 2026
881f7c7
fix(ado-proxy): gate proxy and wrapped az on read permission
jamesadevine Aug 5, 2026
a3f1122
test(smoke): remove obsolete unproxied azure-cli case
jamesadevine Aug 5, 2026
78a90d8
feat(audit): analyze ado-proxy runtime diagnostics
jamesadevine Aug 5, 2026
e5e8279
docs(audit): document ado-proxy diagnostics
jamesadevine Aug 5, 2026
57aed65
fix(audit): handle real artifact download layouts
jamesadevine Aug 5, 2026
b1e5f2a
Merge remote-tracking branch 'origin/main' into devinejames/mcpg-az
Copilot Aug 6, 2026
38d6885
refactor(compile): separate ADO org script from YAML layout
jamesadevine Aug 6, 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
8 changes: 6 additions & 2 deletions .github/workflows/ado-script.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ on:
- "scripts/ado-script/**"
- "src/compile/filter_ir.rs"
- "src/compile/extensions/ado_script.rs"
- "src/ado_proxy/**"
- "src/main.rs"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/ado-script.yml"
Expand All @@ -20,6 +22,8 @@ on:
- "scripts/ado-script/**"
- "src/compile/filter_ir.rs"
- "src/compile/extensions/ado_script.rs"
- "src/ado_proxy/**"
- "src/main.rs"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/ado-script.yml"
Expand Down Expand Up @@ -54,9 +58,9 @@ jobs:

- name: Verify generated TypeScript is up to date
run: |
if ! git diff --exit-code -- scripts/ado-script/src/shared/types.gen.ts scripts/ado-script/src/trigger-e2e/fact-catalog.gen.json; then
if ! git diff --exit-code -- scripts/ado-script/src/shared/types.gen.ts scripts/ado-script/src/trigger-e2e/fact-catalog.gen.json scripts/ado-script/src/shared/ado-proxy-catalog.types.gen.ts scripts/ado-script/src/ado-proxy/catalog.gen.json; then
echo ""
echo "::error::Generated files are out of date with the Rust IR (types.gen.ts and/or fact-catalog.gen.json)."
echo "::error::Generated files are out of date with the Rust source (types.gen.ts, fact-catalog.gen.json, and/or the ado-proxy catalog artifacts)."
echo "Run 'cd scripts/ado-script && npm run codegen' and commit the result."
exit 1
fi
Expand Down
28 changes: 24 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ repository. The pipeline yaml references the agent.
Every compiled pipeline runs as three sequential jobs:

1. **Agent (Stage 1)** — runs the AI agent inside an AWF network-isolated
sandbox with a read-only ADO token. The agent produces *safe-output
proposals* (e.g. "create this PR", "comment on this work item") rather than
acting directly.
sandbox. When configured, a trusted `ado-proxy` process holds the Stage 1
ADO credential and injects it only after a scoped read-policy decision; the
raw token is not injected into the Agent, MCPG, MCP container, or wrapped
`az`. The agent produces *safe-output proposals* (e.g. "create this PR",
"comment on this work item") rather than acting directly.
2. **Detection (Stage 2)** — by default, a separate agent inspects Stage 1's
proposals for prompt injection, secret leaks, and other threats. Authors can
configure or explicitly disable AI analysis under
Expand Down Expand Up @@ -100,7 +102,7 @@ fail-closed and only pauses when the agent actually proposed a reviewed output.
│ │ │ │ ├── repo.rs # RepoContextContributor — repository identity / remote facts
│ │ │ │ ├── schedule.rs # ScheduleContextContributor — scheduled-run context facts
│ │ │ │ └── workitem.rs # WorkItemContextContributor — linked work-item context facts
│ │ │ ├── azure_cli.rs # Always-on Azure CLI extension (runtime detection, AWF mounts, az allowlist)
│ │ │ ├── azure_cli.rs # permissions.read-gated Azure CLI extension (runtime detection, wrapper, AWF mounts, az policy prompt)
│ │ │ └── tests.rs # Extension integration tests
│ │ ├── codemods/ # Front-matter codemods (one file per transformation)
│ │ │ ├── mod.rs # Codemod struct, CODEMODS registry, runner
Expand Down Expand Up @@ -148,6 +150,10 @@ fail-closed and only pauses when the agent actually proposed a reviewed output.
│ ├── ado/ # Shared Azure DevOps REST helpers (auth, list/match/PATCH/POST)
│ │ ├── mod.rs # Shared ADO REST helpers used by all lifecycle commands (`enable`, `disable`, `list`, `status`, `run`, `remove`, `secrets`)
│ │ └── discovery.rs # Project-scope pipeline discovery (`--all-repos` / `--source` flags)
│ ├── ado_proxy/ # Authoritative Stage 1 ADO proxy policy (runtime ships as the `ado-proxy` ado-script bundle; see docs/ado-proxy-design.md)
│ │ ├── mod.rs # Module entry; why the runtime is TypeScript; the compiler/sidecar anti-divergence contract
│ │ ├── catalog.rs # Versioned deny-by-default read-operation catalog (surfaced by `ado-aw catalog --kind ado-proxy`; exported to the bundle as schema + committed snapshot)
│ │ └── policy.rs # Compiler-owned policy document: runtime scope placeholders, capability lowering, explicit cross-org/project scopes, and implicit repository-only grants from type: git repos:
│ ├── audit/ # `ado-aw audit` command — downloads pipeline artifacts and runs analyzers
│ │ ├── mod.rs # Module entry; declares submodules; re-exports `model::*` and CLI helpers
│ │ ├── cli.rs # CLI entry point for the `audit` subcommand
Expand Down Expand Up @@ -288,6 +294,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 dual-ref fallback to make the merge-base reachable; SafeOutputs mode fetches only the target worktree tip
│ ├── 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)
├── tests/ # Integration tests and fixtures
Expand Down Expand Up @@ -407,6 +414,9 @@ index to jump to the right page.
allowed domains, ecosystem identifiers, blocking, repository-resource
`endpoint:` service connections, and ADO `permissions:` service-connection
model.
- [`docs/ado-proxy-design.md`](docs/ado-proxy-design.md) —
security contract and implementation design for credential-isolated
Stage 1 Azure DevOps HTTP access.
- [`docs/extending.md`](docs/extending.md) — adding new CLI commands, compile
targets, front-matter fields, typed IR extensions, safe-output tools,
first-class tools, and runtimes; the `CompilerExtension` trait.
Expand Down Expand Up @@ -482,6 +492,16 @@ Following the gh-aw security model:
agent pool's normal network, so they do **not** need entries in the AWF
allowlist. Air-gapping the build agent itself from GitHub/GHCR is the agent
pool's network policy, not AWF.
**Contributor warning — AWF's chroot makes runner `/tmp` agent-readable.**
The agent's root is the host's `/host` bind mount, and AWF mounts the same
runner `/tmp` at both `/tmp` and `/host/tmp` (`agent-service.ts`). Therefore
anything a host pipeline step writes under runner `/tmp` is visible inside
the agent sandbox. Never stage bearer tokens, CA private keys, WIF
assertions, service-connection material, or other credentials there and
assume deletion will make the exchange safe. This trap has caused repeated
incorrect designs in credential-bearing work. Stream private material over
stdin or use a container-private volume; publish only intentionally public
files (for example the interception CA certificate) under `/tmp`.
3. **Tool Allow-listing**: Agents have access to a limited, controlled set of
tools — see [`docs/tools.md`](docs/tools.md) and
[`docs/mcp.md`](docs/mcp.md).
Expand Down
51 changes: 28 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ DevOps pipeline built around three core security stages:
│ │ │ │ │ │
│ • Runs inside AWF │ │ • Reviews proposed │ │ • Creates PRs │
│ network sandbox │ │ actions for safety│ │ • Creates work items │
│ • Read-only ADO token │ │ • Checks for prompt │ │ • Write ADO token │
│ • Scoped ADO tools │ │ • Checks for prompt │ │ • Write ADO token │
│ • Produces safe │ │ injection, leaks │ │ • Never exposed to │
│ output proposals │ │ │ │ the agent │
└────────────────────────┘ └──────────────────────┘ └───────────────────────┘
Expand Down Expand Up @@ -176,42 +176,47 @@ Push both files to your Azure DevOps repository.
### Step 3: Set Up ARM Service Connections for Permissions

This is the most important configuration step. Azure DevOps does not support
fine-grained PAT scoping — tokens are either read or read-write across the
project. To maintain security isolation between the agent and the executor,
**you need two separate ARM service connections**:
an AAD token whose "read-only" status is implied by the ARM service-connection
name. Azure DevOps authorizes the identity separately from its Azure RBAC
scope. Configure only the connections the workflow needs and grant their
underlying identities the minimum Azure DevOps permissions.

#### Why Two Connections?
#### Connection Roles

| | Read Connection | Write Connection |
|---|---|---|
| **Used by** | Stage 1 — the AI agent | Stage 3 — the safe outputs executor |
| **Purpose** | Query ADO APIs (work items, repos, PRs) | Create PRs, work items, link artifacts |
| **Exposed to agent?** | ✅ Yes (inside network sandbox) | ❌ Never |
| **Used by** | Trusted Stage 1 `ado-proxy` process | Stage 3 safe outputs executor |
| **Purpose** | Authenticate catalogued ADO reads from MCP tools and wrapped `az` | Create PRs, work items, link artifacts |
| **Exposed to agent?** | Raw token: no; scoped read tools: yes | No |
| **Token variable** | `SC_READ_TOKEN` | `SC_WRITE_TOKEN` |
| **Front matter field** | `permissions.read` | `permissions.write` |

The agent runs in a network-isolated sandbox (AWF) with only the read token.
Even if the agent were compromised or prompt-injected, it cannot perform write
operations. Write actions are only executed in Stage 3 (`SafeOutputs`)
after threat analysis, using a completely separate token that the agent never
sees.
The raw Stage 1 token is delivered only to `ado-proxy` over stdin — never to
the Agent, MCPG, Azure DevOps MCP container, or wrapped `az`. The proxy
enforces a deny-by-default read catalog and organization-relative scope tree
before attaching the bearer. Operators must still configure the identity as
least-privileged: the proxy constrains the agent path, while Azure DevOps
remains the upstream authorization boundary. Writes belong in Stage 3
(`SafeOutputs`) after threat analysis.

#### Creating the Service Connections

1. **Navigate** to **Project Settings → Service connections → New service connection**
2. Choose **Azure Resource Manager → Service principal (automatic)** (or manual if
your organization requires it)
3. Create two connections:
3. Create the connections your workflow needs:

**Read connection** (e.g., `ado-agent-read`):
- Scope: subscription or resource group level
- Grants: the ability to mint read-only ADO-scoped tokens
- Used by: the agent job to call `az account get-access-token` with the
ADO resource ID (`499b84ac-1321-427f-aa17-267ca6975798`)
- Used by: the Agent job to mint an ADO-audience token for the trusted
`ado-proxy` process (`499b84ac-1321-427f-aa17-267ca6975798`)
- Required ADO setup: grant the underlying identity only the Azure DevOps
read permissions the workflow needs; the ARM scope does not enforce this

**Write connection** (e.g., `ado-agent-write`):
- Scope: subscription or resource group level
- Grants: the ability to mint read-write ADO-scoped tokens
- Used to mint an ADO-audience token whose effective permissions come from
the underlying identity's Azure DevOps grants
- Used by: the executor job to create PRs, work items, etc.

4. **Reference them** in your agent front matter:
Expand All @@ -231,12 +236,12 @@ sees.

#### Permission Combinations

| Configuration | Agent can read ADO? | Safe outputs can write? |
| Configuration | Scoped Stage 1 ADO reads work? | Safe outputs can write? |
|---|---|---|
| Both `read` + `write` | ✅ | ✅ (via ARM-minted token) |
| Only `read` | ✅ | ✅ (via `$(System.AccessToken)`) |
| Only `write` | | (via ARM-minted token) |
| Neither (default) | | (via `$(System.AccessToken)`) |
| Both `read` + `write` | Yes, when `tools.azure-devops` is enabled | Yes (via ARM-minted token) |
| Only `read` | Yes, when `tools.azure-devops` is enabled | Yes (via `$(System.AccessToken)`) |
| Only `write` | No | Yes (via ARM-minted token) |
| Neither (default) | No | Yes (via `$(System.AccessToken)`) |

### Step 4: Authorize the Pipeline

Expand Down
Loading
Loading