Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ fail-closed and only pauses when the agent actually proposed a reviewed output.
│ ├── github-app-token/ # GitHub App token minter (bundled to github-app-token.js; mints installation token in Agent + Detection when engine.github-app-token is set)
│ ├── 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
│ ├── 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
│ ├── 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
2 changes: 1 addition & 1 deletion docs/ado-proxy-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ verify the first certificate`.

### Credential delivery

`generate_acquire_ado_token` emits an `AzureCLI@2`
`acquire_ado_token_step` emits an `AzureCLI@3`
step that mints an ADO-audience token from the ARM service connection and stores
it as the secret pipeline variable `SC_READ_TOKEN`.

Expand Down
11 changes: 7 additions & 4 deletions docs/ado-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,13 @@ pipeline** as runtime helpers. Today it produces thirteen bundles:
shallow source/target ranges and verifies the base locally. Ineligible or
unavailable REST falls back to bounded dual-ref depths 200/500/2000, never an
automatic full-history fetch. SafeOutputs `target-worktree` mode fetches only
the target tip at depth 1. Each allowed repo is passed as a typed
`--repo-dir` / `--source-ref` / `--target-branch` tuple; per-dir failures are
isolated and surfaced as ADO warnings. The bearer
(`SYSTEM_ACCESSTOKEN`) remains in masked env and spawned-git
the target tip at depth 1. Cross-org repositories are partitioned into a
separate trusted credential scope and passed with validated
organization/project/repository coordinates. The checkout remote must match
those coordinates exactly before the Bearer reaches REST or git; mismatch or
preparation failure stops the trusted task before Agent/executor execution.
Same-org per-dir failures remain isolated warnings. The bearer remains
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).

Expand Down
39 changes: 23 additions & 16 deletions docs/front-matter.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ network: # optional network policy (standalone target only
# variable-groups: # optional: import ADO Library variable groups (standalone/1es only)
# - My Variable Group # each entry must be the exact ADO Library group name (see "Variable Groups" section)
permissions: # optional ADO access token configuration (see docs/network.md#permissions-ado-access-tokens)
read: my-read-arm-connection # shorthand: proxy gets the ARM SC token; Agent/MCP/az get no real token
read: my-read-arm-connection # shorthand: AzureCLI@3 connectionType azureRM
# read: # object form: narrow capabilities / add cross-org or project scope
# service-connection: my-read-arm-connection
# capabilities: [core, repos] # discovery is always enabled
Expand All @@ -268,10 +268,15 @@ permissions: # optional ADO access token configuration (see do
# - project: Shared
# project-id: 33333333-3333-3333-3333-333333333333 # optional GUID-form calls
# repositories: [shared-api] # empty/omitted => project reads only
write: my-write-arm-connection # OPTIONAL ARM SC for Stage 3 executor writes.
# Default: executor uses $(System.AccessToken).
# Set this only for cross-org writes or
# named-identity attribution.
write: my-write-arm-connection # shorthand: AzureCLI@3 connectionType azureRM
# write: # expanded form for scoped cross-org repo writes
# service-connection: ado-repository-writer
# connection-type: azureDevOps
# allow:
# - organization: partner-org
# projects:
# - project: Shared
# repositories: [shared-api]
# permissions-required: # optional abstract capability requirements (usually set by an
# read: true # imported component rather than authored directly); see
# write: true # docs/imports.md#permissions-required. Unioned across all
Expand Down Expand Up @@ -539,19 +544,20 @@ Each entry can be:

| Form | Syntax | Description |
|------|--------|-------------|
| **Shorthand** | `- org/repo` | Alias derived from last segment, type=git, ref=refs/heads/main, checkout=true |
| **Shorthand with alias** | `- alias=org/repo` | Explicit alias before `=` |
| **Object** | `- name: org/repo` | Full control over all fields |
| **Shorthand** | `- project/repo` | Alias derived from last segment, type=git, ref=refs/heads/main, checkout=true |
| **Shorthand with alias** | `- alias=project/repo` | Explicit alias before `=` |
| **Object** | `- name: project/repo` | Full control over all fields |

Object fields:

| Field | Default | Description |
|---------------|------------------------|-------------|
| `name` | *(required)* | Full `org/repo` name (maps to ADO `name:`) |
| `name` | *(required)* | Azure Repos `project/repository` name (maps to ADO `name:`) |
| `alias` | last segment of `name` | Repository alias (maps to ADO `repository:`) |
| `type` | `git` | ADO repository resource type |
| `ref` | `refs/heads/main` | Branch or tag reference |
| `endpoint` | *(none)* | Azure DevOps service connection. Required for `type: github`, `githubenterprise`, or `bitbucket`; not needed for same-org Azure Repos `git`. |
| `endpoint` | *(none)* | Checkout service connection. Required for external providers and cross-organization Azure Repos `git`. |
| `organization` | *(current organization)* | Target Azure DevOps organization for cross-org `type: git`; requires object form and `endpoint`. |
| `checkout` | `true` | Whether the agent job clones this repo |
| `fetch-depth` | *(ADO default)* | Shallow-clone depth for this repo's checkout (ADO `fetchDepth`). `0` = full history |
| `fetch-tags` | *(ADO default)* | Whether to fetch git tags during checkout (ADO `fetchTags`) |
Expand All @@ -560,12 +566,13 @@ Aliases must be unique case-insensitively because they become checkout
directory names on Windows agents. `root`, `repo`, and `self` are reserved in
every casing; `self` is the compiler-owned path for the pipeline repository.

> **Cross-organization `type: git` repositories.** A `type: git` entry with an
> `endpoint:` set (used for a repository outside the pipeline's own Azure
> DevOps organization) checks out correctly, but `create-pull-request` cannot
> yet target it: Stage 3 composes every ADO Git REST call from the pipeline's
> own organization/project. See the limitation note under
> [`create-pull-request`](safe-outputs.md#create-pull-request).
> **Cross-organization `type: git` repositories.** Use object form with both
> `organization:` and `endpoint:`. Checkout authorization and Stage 3 writes
> are separate: `endpoint` authenticates the repository resource, while
> expanded `permissions.write` with `connection-type: azureDevOps` and an exact
> organization/project/repository `allow` scope authorizes `create-pull-request`,
> `create-branch`, and `create-git-tag`. Incomplete entries compile with a
> warning and are rejected if targeted, including under `--dry-run`.

### Tuning checkout fetch behavior (`fetch-depth` / `fetch-tags`)

Expand Down
56 changes: 49 additions & 7 deletions docs/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,8 @@ See [`imports:`](imports.md) for the ADO-first compile-time `repository` and

## Permissions (ADO Access Tokens)

The ARM service-connection scope does not determine what its identity may do in
Azure DevOps. `permissions.read` and `permissions.write` describe intended
The service-connection resource scope does not determine what its identity may
do in Azure DevOps. `permissions.read` and `permissions.write` describe intended
pipeline roles and token placement; operators must separately grant each
underlying identity the minimum Azure DevOps permissions. The executor
(Stage 3) always has a write-capable token; what changes is its *source* and
Expand All @@ -249,7 +249,7 @@ underlying identity the minimum Azure DevOps permissions. The executor
| Source | When | Identity |
| ----------------------------------- | --------------------------------------------- | ----------------------------------------------- |
| `$(System.AccessToken)` *(default)* | No `permissions.write` configured | `Project Collection Build Service (org)` |
| `$(SC_WRITE_TOKEN)` *(opt-in)* | `permissions.write: <arm-service-connection>` | The federated identity behind the ARM SC |
| `$(SC_WRITE_TOKEN)` *(opt-in)* | Any `permissions.write` service connection | The configured connection's Entra identity |

The agent (Stage 1) never receives the executor's token. Stage separation —
not token type — is the trust boundary.
Expand All @@ -261,7 +261,7 @@ not token type — is the trust boundary.
don't match (`PATCH _apis/build/builds/{id}`) and fetches PR metadata for
Tier 2 filters (labels, draft status, changed files). Runs before the
agent, outside the AWF sandbox.
2. **Stage 3 executor** — when no ARM write SC is configured (the default),
2. **Stage 3 executor** — when no write service connection is configured (the default),
the executor's `SYSTEM_ACCESSTOKEN` env var is sourced from
`$(System.AccessToken)`.

Expand All @@ -287,9 +287,11 @@ agents. Set `permissions.write` only when you need:

1. **Cross-org or cross-project writes** — `System.AccessToken` is scoped to
the host project. Targeting work items or repos in a different ADO
project / organization requires an ARM SC with broader scope.
project may use an Azure Resource Manager connection. Cross-organization
repository writes require an Azure DevOps connection and explicit allow
scope.
2. **Named-identity attribution** — `System.AccessToken` writes are
attributed to the `Project Collection Build Service` identity. An ARM SC
attributed to the `Project Collection Build Service` identity. A service connection
attributes writes to its underlying federated identity (e.g.
`safe-output-bot@contoso.com`), useful when audit logs or work-item
notifications need a specific actor.
Expand Down Expand Up @@ -343,6 +345,35 @@ agents. Set `permissions.write` only when you need:
used **only** by the executor in Stage 3 (`SafeOutputs` job). Overrides
the default `$(System.AccessToken)` for write operations. Never exposed
to the agent.

Scalar form preserves the Azure Resource Manager connection contract:

```yaml
permissions:
write: my-arm-connection
```

Expanded form selects AzureCLI@3's connection type and adds exact
cross-organization repository scopes:

```yaml
permissions:
write:
service-connection: ado-repository-writer
connection-type: azureDevOps
allow:
- organization: other-org
projects:
- project: Other Project
repositories: [target-repo]
```

`connection-type` is either `azureRM` or `azureDevOps`. Only
`azureDevOps` permits cross-organization repository writes. The Azure DevOps
service connection must be backed by Entra workload identity federation; its
identity must belong to the same tenant, be added to every target
organization, and receive repository ACLs there. `allow` is additive to the
implicit current organization and is deny-by-default for cross-org targets.
- **Both omitted**: The agent has no ADO API access. The executor still has
a write-capable token via `$(System.AccessToken)`, scoped by the
pipeline's job-authorization settings.
Expand All @@ -354,11 +385,22 @@ agents. Set `permissions.write` only when you need:
permissions:
read: my-read-sc

# Cross-org / named-identity attribution — executor writes via ARM SC.
# Named-identity attribution through an Azure Resource Manager connection.
permissions:
read: my-read-sc
write: my-write-sc

# Cross-org repository writes through an Azure DevOps WIF connection.
permissions:
write:
service-connection: ado-repository-writer
connection-type: azureDevOps
allow:
- organization: other-org
projects:
- project: Other Project
repositories: [target-repo]

# Agent has no ADO read access; executor still writes via $(System.AccessToken).
# (Empty front matter — no `permissions:` key at all.)
```
29 changes: 22 additions & 7 deletions docs/safe-output-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ The toggle lives in three places (most-specific wins):
> which carries `PullRequestContribute` by default.

If `permissions.write:` is set in the agent's front matter, Stage 3
uses the **ARM service connection's identity** instead, and none of
uses the configured **service connection's identity** instead, and none of
the above applies — see [Option 1](#option-1-wire-a-write-service-connection-recommended).

---
Expand Down Expand Up @@ -230,24 +230,39 @@ complementary.

### Option 1: Wire a write service connection (recommended)

Add an ARM service connection whose backing identity has the
permission you need on the target repository, and reference it from
the agent front matter:
For same-organization or same-organization cross-project writes, the scalar
form keeps the Azure Resource Manager connection behavior:

```yaml
permissions:
read: ado-aw-read # optional, used by Stage 1
write: ado-aw-write # used by Stage 3
```

For cross-organization repository writes, use an Azure DevOps service
connection backed by Entra workload identity federation:

```yaml
permissions:
write:
service-connection: ado-aw-repository-writer
connection-type: azureDevOps
allow:
- organization: other-org
projects:
- project: Other Project
repositories: [target-repo]
```

Stage 3 will mint its token via that connection instead of using
`$(System.AccessToken)`, so the build-service ACEs become irrelevant.

This is the most explicit option: the identity used for writes is
named in the front matter, audit logs attribute every action to that
named principal, and the least-privilege grant lives entirely on the
service connection's identity. It also works unchanged for
cross-organization writes.
service connection's identity. The same identity can write in multiple
same-tenant organizations after it is added to each organization and granted
the repository permissions listed above.

See [`docs/network.md`](network.md) (Permissions section) and the
"Service Connections" page on the documentation site for the full
Expand Down Expand Up @@ -306,7 +321,7 @@ Option 2 unless you have a specific reason to broaden the grant.

| HTTP status | Body fragment | Most likely cause |
|---|---|---|
| 401 Unauthorized | `TF400813: The user '...' is not authorized to access this resource` | Token is malformed or missing — usually a misconfigured service-connection step; check that the AzureCLI@2 mint succeeded. |
| 401 Unauthorized | `TF400813: The user '...' is not authorized to access this resource` | Token is malformed or missing — usually a misconfigured service-connection step; check that the AzureCLI@3 mint succeeded. |
| 403 Forbidden | `TF401027: You need the Git 'PullRequestContribute' permission` | This page — Stage 3 identity lacks PR-contribute on the target repo. |
| 403 Forbidden | `TF401027: You need the Git 'GenericContribute' permission` | Same diagnosis; need `Contribute` on the repo (typically because of `create-pull-request` or `create-branch`). |
| 403 Forbidden | `VS800075: The project ... does not exist, or you do not have permission to access it.` | Cross-project request blocked because "Limit job authorization scope to current project" is ON. Use Option 1 with a write service connection that has cross-project rights, or move the resource into the calling project. |
Expand Down
Loading
Loading