Part of the ado-aw documentation.
The execution-context plugin stages a small, focused set of per-run
context signals on disk and appends a tailored fragment to the agent
prompt before the agent starts. The agent then runs git diff,
git show, git log itself against the precomputed SHAs (the
workspace's .git/objects/ are already populated by the precompute
fetch) and calls Azure DevOps MCP tools with pre-filled identifiers
already embedded in its prompt.
This is an always-on compiler extension. There is no tools: entry to
enable it; per-trigger contributors gate themselves based on the
agent's on: configuration.
Background and motivation: this feature was tracked in issue #860.
PR-reviewer agents almost always need the same precondition: a fully
fetched target branch and resolved base / head SHAs. ADO's default
checkout: self is shallow (fetchDepth: 1), doesn't fetch the PR
target branch, and (deliberately) does not persist credentials into
.git/config for OAuth bearer reuse. Every PR-reviewer agent has
historically rebuilt the same ~120 lines of bash to work around this.
The execution-context plugin owns that step centrally — but does only the part the agent cannot do for itself:
- Fetches the PR target branch with progressive deepening until
git merge-baseresolves (requires the bearer; cannot happen inside the agent's sandbox). - Writes the resolved
base.shaandhead.shaso the agent can reuse them across manygit diffinvocations. - Appends a prompt fragment listing the right
gitcommands and ADO MCP tool calls (with literal PR id / project / repo interpolated) for the agent to use.
The agent does its own diff/show/log/stat work — it has the objects
locally and git is added to its bash allow-list automatically.
| Contributor | Trigger | Output layout |
|---|---|---|
pr |
on.pr |
aw-context/pr/* |
Future trigger contributors (pipeline-completion, schedule, manual)
plug in via the same internal ContextContributor trait without
breaking the agent-facing layout.
execution-context:
enabled: true # master switch; defaults to true
pr:
enabled: true # defaults to true when `on.pr` is configuredAll keys are optional. When the execution-context: block is omitted
entirely, defaults are "on for the triggers configured in on:".
enabled(bool, defaulttrue) — master switch. Whenfalse, no contributor runs and noaw-context/is staged.pr.enabled(bool, defaulttruewhenon.pris set) — whether to activate the PR contributor. Setfalseto opt out (e.g. when an agent already does its own precompute or doesn't need PR context).on.prmust be configured for the contributor to activate at all —pr.enabled: truewithout anon.prtrigger has no effect (the prepare step would be dead code, and silently widening the agent's bash allow-list with git commands for a non-PR agent would be a footgun).
pr.enabled: false also suppresses the auto-extension of the agent's
bash allow-list with git commands described below.
For PR-triggered builds, the precompute step stages files under
$(Build.SourcesDirectory)/aw-context/ (i.e. relative to the agent's
working directory):
aw-context/
pr/
base.sha # PR merge-base SHA (40-char hex, no trailing newline)
head.sha # PR head SHA (40-char hex, no trailing newline)
base.sha is the common ancestor of the PR head and the PR target
branch — git merge-base in both the synthetic-merge-commit path and
the progressive-deepening path. This makes git diff $BASE..$HEAD
produce the SAME change set regardless of whether ADO checked out a
real branch tip or a synthetic merge commit (i.e. the diff is "what
the PR introduces since branch-point", not "what the PR introduces
versus the current target tip").
aw-context/
pr/
error.txt # one-line failure reason
(base.sha / head.sha are not written on failure.)
Short identifiers — PR id, ADO project name, ADO repository name — are not staged as files. They are interpolated directly into the agent prompt fragment ("This is PR #4242 in project 'OneBranch' / repository 'my-repo'…"), so the agent sees them as natural English and as literal arguments in example ADO MCP tool calls. Files are reserved for the opaque 40-char SHAs the agent reuses across many commands.
The precompute step appends one of two fragments directly to
/tmp/awf-tools/agent-prompt.md (the file built by the
"Prepare agent prompt" step in base.yml). This mirrors how gh-aw
injects its own built-in prompt sections.
The fragment shows how to set $BASE / $HEAD from the staged files,
lists six common git invocations (diff --stat, diff --name-status, diff, diff -- <path>, show $HEAD:<path>, log),
and shows three example ADO MCP tool calls
(repo_get_pull_request_by_id, repo_list_pull_request_threads,
repo_create_pull_request_thread) with project, repositoryId,
and pullRequestId pre-filled to the actual values.
When the precompute fails (identifier validation or merge-base
resolution exhausts the depth budget), the failure fragment is
appended instead. It states the reason from aw-context/pr/error.txt
and tells the agent:
- Local
git diffis unavailable for this run. - ADO MCP tool calls remain possible (the PR id / project / repo are still embedded in the fragment).
- Do NOT produce an empty review or pretend the PR has no changes —
surface the failure (e.g. via
report_incomplete) or fall back to the API.
If neither fragment is appended (Build.Reason ≠ PullRequest), the agent prompt is silent on PR context.
When the PR contributor activates, these read-only git commands
are added to the agent's bash allow-list:
git, git diff, git log, git show, git status, git rev-parse, git symbolic-ref
The extension uses the same required_bash_commands() plumbing as
the runtime extensions (Python, Node, .NET, Lean). When the agent has:
tools.bash setting |
Behaviour |
|---|---|
bash: (omitted or wildcard) |
Allow-all mode — extension is a no-op (commands are already permitted). |
bash: ["..."] (explicit list) |
The 7 git commands are appended to the user's list. |
pr.enabled: false |
The 7 git commands are NOT added (matches the contributor's overall inactive state). |
This keeps the agent's bash surface intentional: opting out of the PR contributor opts out of the corresponding git capability.
The PR contributor's prepare step is a 4-line bash wrapper that
invokes node /tmp/ado-aw-scripts/ado-script/exec-context-pr.js
with SYSTEM_ACCESSTOKEN plus the five SYSTEM_* / BUILD_*
identifier env vars passed through. The actual work lives in the
exec-context-pr.js bundle
under scripts/ado-script/src/exec-context-pr/. The bundle:
- Reads
System.PullRequest.*andSystem.TeamProject/Build.Repository.Namefrom the environment. No manual ref discovery — ADO already populates these. - Validates identifiers with strict allowlist regexes
(
PR_ID⊆ digits,PROJECT/REPO⊆ alphanumeric +._-,PROJECTadditionally allows space,PR_TARGET_BRANCH⊆ alphanumeric +._/-). Seevalidate.ts. Failure writeserror.txtand appends the failure prompt fragment. - Detects merge-commit shape. If
HEADhas ≥ 3 tokens ingit rev-list --parents HEAD(the synthetic merge commit ADO checks out for PR builds), usesHEAD^2as the PR head and computesgit merge-base HEAD^1 HEAD^2as the base — same semantics as the deepening path, no target-branch fetch needed. Otherwise: - Fetches the PR target branch with progressive deepening —
--depth=200, then500, then2000, then finally--unshallow. After each successful fetch, attemptsgit merge-base origin/<target> HEADand continues to the next depth if it cannot resolve yet. Seemerge-base.ts. - Writes
base.shaandhead.shaon success and appends the success prompt fragment to/tmp/awf-tools/agent-prompt.md(path overridable viaAW_AGENT_PROMPT_FILEfor tests). Seeprompt.ts. - On failure, writes
error.txtand appends the failure prompt fragment.
The bundle exits 0 in both success and failure paths so the build
proceeds — the agent surfaces failures via the prompt fragment, not
via a build break. The only exit-1 path is a hard infrastructure
failure (e.g. the workspace root is not writable, so the mkdir -p aw-context/pr cannot be created); the wrapper bash's set -euo pipefail propagates that to the pipeline.
The whole step is gated by condition: eq(variables['Build.Reason'], 'PullRequest') so it is a no-op on manual or scheduled queues of a
PR-triggered pipeline.
The previous incarnation embedded ~190 lines of bash heredoc into the emitted YAML, with only end-to-end shellcheck for coverage. The TS port gains:
- Unit-test coverage — 32 vitest tests across
validate.ts,git.ts,merge-base.ts,prompt.tsplus 3 end-to-end smoke tests that exercise a synthetic-merge git repo. - Tighter trust boundary — the bearer lives only in the Node
process's env and is injected into the spawned
gitchild viaGIT_CONFIG_*env vars (git.ts::bearerEnv), not into the wrapping bash shell. - Smaller emitted YAML —
pr.rsshrinks from ~320 lines to ~145 lines; the emitted step body is 4 lines instead of ~190.
The bundle is installed and downloaded into the Agent job by
AdoScriptExtension, which fires whenever either import.js or
exec-context-pr.js is needed. See
ado-script.md.
The PR contributor must fetch the PR target branch (which the default checkout does not), but doing so requires an OAuth bearer. ado-aw preserves the Stage 1 read-only invariant with these design choices:
| Mechanism | Decision |
|---|---|
Override checkout: self with persistCredentials: true |
Rejected. It would write the build identity's bearer into .git/config inside the workspace, which is then mounted into the AWF sandbox where the agent could read and exfiltrate it. |
Override checkout: self with fetchDepth: 0 |
Rejected. Unnecessary — the precompute fetches exactly the refs it needs. |
In-step SYSTEM_ACCESSTOKEN + GIT_CONFIG_* bearer env |
Adopted. SYSTEM_ACCESSTOKEN is mapped from $(System.AccessToken) only into the node exec-context-pr.js step's process env. The bundle's git.ts::bearerEnv then injects GIT_CONFIG_COUNT / GIT_CONFIG_KEY_0 / GIT_CONFIG_VALUE_0 into the spawned git child process's env only — not into the Node process's own env, and never via git -c on argv. The token never appears in process listings and is never written to disk. After the Node process exits, the bearer is gone from the runtime environment the agent inherits. |
After the precompute step exits, the bearer is gone from the runtime
environment the agent inherits, .git/config contains no
http.extraheader line, and the agent container is started by AWF
with its own (read-only) MI from the ARM service connection.
The compile-time test
test_execution_context_pr_does_not_leak_system_accesstoken walks
the generated YAML and asserts that SYSTEM_ACCESSTOKEN appears
only in the execution-context prepare step's env: block, never
the agent step's.
If you have an existing PR-reviewer agent with a steps: block that
manually fetches the target branch and resolves merge-base: delete
that block, ensure on.pr is configured, and let the agent read
aw-context/pr/{base,head}.sha directly. The prompt fragment is
appended automatically — you do not need to mention the layout in
your own markdown body.
- Identifiers in the prompt, SHAs on disk. Short values (PR id,
project, repo) are interpolated into the prompt heredoc; long
opaque 40-char SHAs stay as files where shell ergonomics actually
win (
BASE=$(cat aw-context/pr/base.sha)is the natural pattern). - Non-
selfcheckouts inrepos:. v1 only diffs theselfcheckout. The PR contributor does not currently produce contexts for additional repository checkouts. - Workspace alias. When
workspace:points to a non-selfalias,aw-context/is still relative to$(Build.SourcesDirectory)— i.e. the pipeline's working directory, not the workspace alias's directory. - Ordering. The precompute step runs after
{{ checkout_self }}in the Agent job's prepare phase, after the "Prepare agent prompt" step (so it can append) and before the agent runs (so the agent sees the appended prompt).
- Always-on
ExecContextExtensioninsrc/compile/extensions/exec_context/mod.rs(ExtensionPhase::Tool). - Internal
ContextContributortrait incontributor.rs. v1 ships one contributor:PrContextContributorinpr.rs. - Front-matter types:
ExecutionContextConfigandPrContextConfiginsrc/compile/types.rs(PrContextConfigis just{ enabled: Option<bool> }). - Compile tests live in
tests/compiler_tests.rs(search fortest_execution_context_pr_*). - The generated bash is shellchecked by
tests/bash_lint_tests.rsvia theexecution-context-agent.mdfixture.