Skip to content

Commit 3de0690

Browse files
authored
feat(compile): add execution-context plugin with PR contributor (#860) (#865)
1 parent 9187fc2 commit 3de0690

30 files changed

Lines changed: 3052 additions & 79 deletions

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
run: |
7474
set -euo pipefail
7575
cd scripts
76-
zip -r ../ado-script.zip ado-script/gate.js ado-script/import.js
76+
zip -r ../ado-script.zip ado-script/gate.js ado-script/import.js ado-script/exec-context-pr.js
7777
7878
- name: Upload release assets
7979
env:

AGENTS.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ Every compiled pipeline runs as three sequential jobs:
6262
│ │ │ ├── ado_aw_marker.rs # Always-on metadata marker extension (emits # ado-aw-metadata JSON)
6363
│ │ │ ├── github.rs # Always-on GitHub MCP extension
6464
│ │ │ ├── safe_outputs.rs # Always-on SafeOutputs MCP extension
65-
│ │ │ ├── ado_script.rs # Always-on ado-script extension (gate evaluator + runtime-import resolver, per-job downloads)
65+
│ │ │ ├── ado_script.rs # Always-on ado-script extension (gate evaluator + runtime-import resolver + exec-context-pr precompute, per-job downloads)
66+
│ │ │ ├── exec_context/ # Always-on execution-context extension (issue #860)
67+
│ │ │ │ ├── mod.rs # ExecContextExtension; CompilerExtension impl; contributor fan-out
68+
│ │ │ │ ├── contributor.rs # Internal ContextContributor trait + Contributor enum
69+
│ │ │ │ └── pr.rs # PrContextContributor — stages aw-context/pr/* for PR builds
6670
│ │ │ ├── azure_cli.rs # Always-on Azure CLI extension (runtime detection, AWF mounts, az allowlist)
6771
│ │ │ └── tests.rs # Extension integration tests
6872
│ │ ├── codemods/ # Front-matter codemods (one file per transformation)
@@ -181,10 +185,11 @@ Every compiled pipeline runs as three sequential jobs:
181185
│ ├── update-ado-agentic-workflow.md # Guide for modifying an existing agentic pipeline
182186
│ └── debug-ado-agentic-workflow.md # Guide for troubleshooting a failing agentic pipeline
183187
├── scripts/ # Supporting scripts shipped as release artifacts
184-
│ └── ado-script/ # TypeScript workspace for bundled gate.js, import.js, and future bundles
188+
│ └── ado-script/ # TypeScript workspace for bundled gate.js, import.js, exec-context-pr.js, and future bundles
185189
│ └── src/
186190
│ ├── gate/ # Gate evaluator source (bundled to gate.js)
187191
│ ├── import/ # Runtime prompt resolver source (bundled to import.js)
192+
│ ├── exec-context-pr/ # PR-context precompute source (bundled to exec-context-pr.js)
188193
│ └── shared/ # Shared modules across bundles (auth, ado-client, env-facts, types.gen.ts)
189194
├── tests/ # Integration tests and fixtures
190195
├── docs/ # Per-concept reference documentation (see index below)
@@ -236,6 +241,12 @@ index to jump to the right page.
236241
Python, Node.js, .NET).
237242
- [`docs/targets.md`](docs/targets.md) — target platforms: `standalone`,
238243
`1es`, `job`, and `stage`.
244+
- [`docs/execution-context.md`](docs/execution-context.md) — built-in
245+
`aw-context/` precompute (issue #860): PR target-branch fetch +
246+
merge-base resolution, `base.sha`/`head.sha` artefacts, prompt
247+
fragment with pre-filled ADO MCP identifiers, auto-extension of the
248+
agent's bash allow-list with read-only git commands; configured via
249+
the `execution-context:` front-matter block.
239250
- [`docs/safe-outputs.md`](docs/safe-outputs.md) — full reference for every
240251
safe-output tool agents can use to propose actions (PRs, work items, wiki
241252
pages, comments, etc.) plus their per-agent configuration.
@@ -272,7 +283,7 @@ index to jump to the right page.
272283
adding codemods.
273284
- [`docs/ado-script.md`](docs/ado-script.md)`ado-script` workspace
274285
(`scripts/ado-script/`): the bundled TypeScript runtime helpers (today:
275-
`gate.js` and `import.js`), schemars-driven type codegen, and the A2 design decision.
286+
`gate.js`, `import.js`, `exec-context-pr.js`), schemars-driven type codegen, and the A2 design decision.
276287
- [`docs/local-development.md`](docs/local-development.md) — local development
277288
setup notes.
278289

docs/ado-script.md

Lines changed: 121 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
`ado-script` is the umbrella name for the TypeScript workspace at
44
[`scripts/ado-script/`](../scripts/ado-script/). It produces small,
55
ncc-bundled Node programs that the **compiler injects into every emitted
6-
pipeline** as runtime helpers. Today it produces `gate.js`, the
7-
trigger-filter gate evaluator, and `import.js`, the runtime prompt
8-
resolver described in [`runtime-imports.md`](runtime-imports.md).
6+
pipeline** as runtime helpers. Today it produces three bundles:
7+
8+
- `gate.js` — trigger-filter gate evaluator (Setup job).
9+
- `import.js` — runtime prompt resolver described in
10+
[`runtime-imports.md`](runtime-imports.md) (Agent job).
11+
- `exec-context-pr.js` — PR-context precompute that resolves the
12+
merge-base, writes `aw-context/pr/{base,head}.sha`, and appends a
13+
prompt fragment to the agent prompt (Agent job, before the agent
14+
runs). See [`execution-context.md`](execution-context.md).
915

1016
> **Internal-only.** `ado-script` is not a user-facing front-matter
1117
> feature. Authors never write an `ado-script:` block in their agent
@@ -52,17 +58,76 @@ because the compiler always embeds an absolute marker path and
5258
not re-expanded).
5359

5460
The bundle lives at `import.js` and ships in the same
55-
`ado-script.zip` release asset as `gate.js`, so pipelines download it
56-
through the same Setup-job asset flow. `import.js` uses only the Node
57-
standard library, so the ncc bundle is small (~1.5 KB) and carries no
58-
SDK dependency.
61+
`ado-script.zip` release asset as `gate.js` and `exec-context-pr.js`,
62+
so pipelines download it through the same Agent-job asset flow.
63+
`import.js` uses only the Node standard library, so the ncc bundle is
64+
small (~1.5 KB) and carries no SDK dependency.
5965

6066
The Stage-2 threat-analysis prompt is **not** runtime-imported.
6167
`src/data/threat-analysis.md` is `include_str!`'d into the `ado-aw`
6268
binary and inlined into the emitted YAML at compile time, matching
6369
gh-aw's pattern (their `threat_detection.md` ships with the setup
6470
action and is read directly from disk — no marker, no resolver).
6571

72+
## What `exec-context-pr.js` does
73+
74+
`exec-context-pr.js` is a single-shot Node program that runs as the
75+
**precompute step** of the PR contributor of the execution-context
76+
extension. It runs in the Agent job *before* the agent step, inside
77+
the AWF network-isolated sandbox's prepare phase.
78+
79+
It performs the work that used to live as ~190 lines of bash heredoc
80+
inside `src/compile/extensions/exec_context/pr.rs`:
81+
82+
1. **Validate identifiers**`PR_ID`, `SYSTEM_TEAMPROJECT`,
83+
`BUILD_REPOSITORY_NAME`, and `SYSTEM_PULLREQUEST_TARGETBRANCH` are
84+
each matched against a strict allowlist regex (`validate.ts`)
85+
before any of them are interpolated into a git refspec or the
86+
agent prompt. On any failure the program writes
87+
`aw-context/pr/error.txt` and a `### PR context (unavailable)`
88+
fragment to the agent prompt, then exits 0 (soft fail: the agent
89+
still runs, but is told the context is missing).
90+
2. **Resolve merge-base** — if the checkout is a synthetic
91+
merge-commit (parent count ≥ 3 per ADO's PR-validation flow),
92+
`merge-base.ts::resolveMergeBase` computes `git merge-base` over
93+
the two parents. Otherwise it fetches the target branch with
94+
progressive deepening (`--depth=200/500/2000/--unshallow`) and
95+
then `git merge-base` against `HEAD`. Same `BASE_SHA` semantics
96+
in both paths (git's true common ancestor).
97+
3. **Stage artefacts** — writes `aw-context/pr/base.sha` and
98+
`aw-context/pr/head.sha` so the agent can `git diff $(cat
99+
.../base.sha)..$(cat .../head.sha)` itself.
100+
4. **Append prompt fragment** — appends a `## PR context` section to
101+
`/tmp/awf-tools/agent-prompt.md` (path overridable via
102+
`AW_AGENT_PROMPT_FILE` for tests).
103+
104+
### Trust boundary
105+
106+
The bearer (`SYSTEM_ACCESSTOKEN`) is mapped into the Node process's
107+
env by the wrapper bash step, but is **only** propagated into the
108+
spawned `git` child process via `GIT_CONFIG_COUNT=1 / KEY_0 /
109+
VALUE_0` env vars (see `git.ts::bearerEnv` + `runGit` in
110+
`merge-base.ts`). It never appears in argv, is never written to
111+
`.git/config`, and is never visible to the agent process (which is
112+
spawned later, in a separate AWF child). The
113+
`test_execution_context_pr_does_not_leak_system_accesstoken` Rust
114+
test walks the emitted YAML and asserts this scoping.
115+
116+
### Env-var contract
117+
118+
| Env var | Source | Purpose |
119+
|---|---|---|
120+
| `SYSTEM_ACCESSTOKEN` | `$(System.AccessToken)` | ADO REST / git fetch bearer |
121+
| `SYSTEM_PULLREQUEST_PULLREQUESTID` | `$(System.PullRequest.PullRequestId)` | PR identifier (validated numeric) |
122+
| `SYSTEM_PULLREQUEST_TARGETBRANCH` | `$(System.PullRequest.TargetBranch)` | PR target branch for the fetch |
123+
| `SYSTEM_TEAMPROJECT` | `$(System.TeamProject)` | ADO project name (validated) |
124+
| `BUILD_REPOSITORY_NAME` | `$(Build.Repository.Name)` | Repository name (validated) |
125+
| `BUILD_SOURCESDIRECTORY` | `$(Build.SourcesDirectory)` | Workspace root for `aw-context/` |
126+
| `AW_AGENT_PROMPT_FILE` | (test override) | Override default `/tmp/awf-tools/agent-prompt.md` |
127+
128+
The bundle uses only `node:child_process` / `node:fs` / `node:path`
129+
— no `azure-devops-node-api`, no `fetch`. The ncc'd bundle is ~8 KB.
130+
66131
## End-to-end data flow
67132

68133
```
@@ -183,20 +248,29 @@ scripts/ado-script/
183248
│ │ ├── facts.ts # fact acquisition (env + REST)
184249
│ │ ├── predicates.ts # 11 predicate evaluators + validatePredicateTree + glob ReDoS hardening
185250
│ │ └── selfcancel.ts # best-effort build cancellation
186-
│ └── import/ # import.js entry point + runtime prompt resolver
187-
│ ├── index.ts # main(): expand runtime-import markers in place
188-
│ └── __tests__/ # marker, path-resolution, and single-pass coverage
189-
├── test/ # End-to-end smoke tests
251+
│ ├── import/ # import.js entry point + runtime prompt resolver
252+
│ │ ├── index.ts # main(): expand runtime-import markers in place
253+
│ │ └── __tests__/ # marker, path-resolution, and single-pass coverage
254+
│ └── exec-context-pr/ # exec-context-pr.js entry point + PR precompute
255+
│ ├── index.ts # main(): validate → resolve merge-base → stage SHAs → append prompt
256+
│ ├── validate.ts # identifier regex guards
257+
│ ├── git.ts # execFile wrappers + bearerEnv helper
258+
│ ├── merge-base.ts # synthetic-merge detection + progressive-deepening fetch
259+
│ ├── prompt.ts # success / failure prompt-fragment writers
260+
│ └── __tests__/ # 32 unit tests across the four modules
261+
├── test/ # End-to-end smoke tests (gate, import, exec-context-pr)
190262
├── gate.js # ncc bundle output (gitignored)
191-
└── import.js # ncc bundle output (gitignored)
263+
├── import.js # ncc bundle output (gitignored)
264+
└── exec-context-pr.js # ncc bundle output (gitignored)
192265
```
193266

194267
The release workflow (`.github/workflows/release.yml`) runs
195-
`npm ci && npm run build`, then zips `scripts/ado-script/gate.js` and
196-
`scripts/ado-script/import.js` into
197-
the `ado-script.zip` release asset. Pipelines download that asset at
198-
runtime by URL pinned to the compiler's `CARGO_PKG_VERSION`, verify
199-
its SHA-256 against the `checksums.txt` asset, then extract.
268+
`npm ci && npm run build`, then zips `scripts/ado-script/gate.js`,
269+
`scripts/ado-script/import.js`, and
270+
`scripts/ado-script/exec-context-pr.js` into the `ado-script.zip`
271+
release asset. Pipelines download that asset at runtime by URL pinned
272+
to the compiler's `CARGO_PKG_VERSION`, verify its SHA-256 against the
273+
`checksums.txt` asset, then extract.
200274

201275
## Schema codegen
202276

@@ -254,11 +328,12 @@ three step strings into the Setup job:
254328
runs the gate with `GATE_SPEC` and the env-var contract documented
255329
above.
256330

257-
### Agent job (runtime-import resolver)
331+
### Agent job (runtime-import resolver + PR-context precompute)
258332

259-
When `inlined-imports: false` (the default), `prepare_steps()` returns
260-
the same install + download pair plus the resolver invocation, into
261-
the Agent job's existing `{{ prepare_steps }}` block:
333+
When `inlined-imports: false` (the default) OR the execution-context
334+
PR contributor activates (`on.pr` configured and not disabled),
335+
`prepare_steps()` returns the install + download pair into the Agent
336+
job's existing `{{ prepare_steps }}` block:
262337

263338
1. **`NodeTool@0`** — same shape as above.
264339
2. **`curl` download + verify + extract** — same artefact, same
@@ -267,24 +342,40 @@ the Agent job's existing `{{ prepare_steps }}` block:
267342
expands `{{#runtime-import …}}` markers in
268343
`/tmp/awf-tools/agent-prompt.md` in place. See
269344
[`runtime-imports.md`](runtime-imports.md) for marker syntax.
345+
**Only emitted when `inlined-imports: false`.**
346+
347+
The PR-context precompute step (`node exec-context-pr.js`) is owned
348+
by `ExecContextExtension` (not `AdoScriptExtension`) and emitted in
349+
its own `Tool`-phase `prepare_steps()`. Phase ordering
350+
(`AdoScriptExtension::phase() == System` < `ExecContextExtension::phase() == Tool`)
351+
guarantees the bundle is installed and on disk before the
352+
exec-context invocation runs.
270353

271354
### Per-job download (NOT a duplication bug)
272355

273356
ADO jobs use **isolated VMs**`/tmp` is not shared between jobs.
274357
The `ado-script.zip` bundle therefore has to be downloaded once per
275-
job that consumes it. When both features are active (a pipeline with
276-
both `filters:` and `inlined-imports: false`), install + download
277-
steps appear in **both** Setup and Agent. That's correct architecture
278-
given ADO's topology, not waste.
358+
job that consumes it. When both Setup and Agent need it, install +
359+
download steps appear in **both**. That's correct architecture given
360+
ADO's topology, not waste.
279361

280362
### What gets emitted, by case
281363

282-
| `filters:` | `inlined-imports` | Setup-job steps | Agent-job extra steps |
364+
| Setup consumer | Agent consumer | Setup-job steps | Agent-job extra steps |
283365
|---|---|---|---|
284-
| inactive | `true` | (none) | (none) |
285-
| inactive | `false` | (no Setup job) | install + download + resolver |
286-
| active | `true` | install + download + gate | (none) |
287-
| active | `false` | install + download + gate | install + download + resolver |
366+
| no gate | none | (none) | (none) |
367+
| no gate | `inlined-imports: false` only | (no Setup job) | install + download + resolver |
368+
| no gate | `on.pr` execution-context only | (no Setup job) | install + download + exec-context-pr |
369+
| no gate | both | (no Setup job) | install + download + resolver + exec-context-pr |
370+
| gate | none | install + download + gate | (none) |
371+
| gate | any combination of resolver / exec-pr | install + download + gate | install + download + (resolver?) + (exec-context-pr?) |
372+
373+
The "Setup consumer" column is gated on `filters:` lowering to non-empty
374+
checks. The "Agent consumer" columns are gated on
375+
`inlined-imports: false` (resolver) and the PR contributor's
376+
activation predicate (exec-context-pr; see
377+
`pr_contributor_will_activate` in
378+
`src/compile/extensions/exec_context/mod.rs`).
288379

289380
The IR-to-bash codegen that produces the gate step is
290381
`compile_gate_step_external` in `src/compile/filter_ir.rs`.

0 commit comments

Comments
 (0)