feat(exec-context): add 7 trigger-context contributors with shared/ refactor#1019
Conversation
…ributors Stage 0 of the execution-context contributor build-out (plan.md, session artifact). Pure file move; no behavioural change. Moves four helper modules from scripts/ado-script/src/exec-context-pr/ into scripts/ado-script/src/shared/ so the upcoming pipeline, ci-push, schedule, and workitem contributors can reuse them without fragmenting the workspace with an exec-context-common/ sibling. Files moved (with git mv, preserving history): - git.ts (bearerEnv + runGit / gitOk wrappers) - merge-base.ts (synthetic-merge detection + progressive-deepening fetch) - validate.ts (identifier allowlist regexes + sanitizeForPrompt) - prompt.ts (success / failure prompt-fragment writers + appender) And their __tests__/ siblings. shared/index.ts gains four new barrels (git, mergeBase, validate, prompt). exec-context-pr/index.ts now imports from ../shared/ instead of ./. Doc references in docs/ado-script.md and src/compile/extensions/exec_context/pr.rs updated to match. Validation: - cargo build / cargo test (2013 tests, 0 failures) / cargo clippy clean - npm run codegen / typecheck / test (283 tests) / build / test:smoke (6 tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stage 1 of the execution-context contributor build-out (plan.md).
Adds the `manual` contributor: activates whenever the agent declares
any `parameters:` block and stages requestor identity + a JSON
snapshot of parameter values for manually-queued ADO builds.
Artefacts staged under `aw-context/manual/`:
- `requested-for` — `Build.RequestedFor` display name
- `requested-for-email` — only when `manual.include-email: true`
- `parameters.json` — `{name: value}` snapshot of user-declared
params (clearMemory is auto-injected at
IR-build time so it is naturally absent)
Front-matter surface (all optional):
execution-context:
manual:
enabled: true # default when parameters: declared
include-email: false # hygiene default; opt-in
Trust boundary: no bearer (SYSTEM_ACCESSTOKEN NOT projected), no
network, no REST. Parameter NAMES are validated as ADO identifiers
upstream and re-checked at emit time by the contributor as
defence-in-depth. Parameter VALUES are JSON-serialised (escape-safe)
when written to disk and sanitised via shared/validate.sanitizeForPrompt
before any prompt interpolation.
Runtime gate: `eq(variables['Build.Reason'], 'Manual')` — non-manual
queues skip the step at zero cost.
Wiring:
- New `ManualContextContributor` in src/compile/extensions/exec_context/manual.rs
- `Contributor::Manual` variant in contributor.rs
- `manual_contributor_will_activate` helper in mod.rs (OR'd into
`any_contributor_active` aggregate; pub-re-exported via
extensions/mod.rs for collect_extensions)
- `EXEC_CONTEXT_MANUAL_PATH` + `exec_context_manual_active: bool`
on AdoScriptExtension; OR'd into the install/download gate so the
bundle ships with the Agent job's prepare-phase setup
- `ManualContextConfig` on ExecutionContextConfig
- New TS bundle scripts/ado-script/src/exec-context-manual/
(with __tests__/index.test.ts; 16 vitest cases)
- `Build.RequestedFor` + `Build.RequestedForEmail` added to
ADO macro allowlist in ir/env.rs
Validation:
- cargo build / cargo test (2025 Rust tests, 0 failures) / clippy clean
- npm typecheck / test (299 tests) / build / smoke (6 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stage 2 of the execution-context contributor build-out (plan.md).
Adds the `pipeline` contributor: activates whenever the agent
declares an `on.pipeline` trigger and stages upstream-build
metadata for `resources.pipelines`-triggered ADO builds.
Also introduces `scripts/ado-script/src/shared/build.ts` — the
Build REST helper module that Stages 3 (ci-push), 5 (schedule), and
6 (pr.checks) will also consume. Two exports today: `getBuildById`,
`listArtifacts`. Both wrap the existing `withRetry` machinery
from shared/ado-client.ts for transient-error resilience.
Artefacts staged under `aw-context/pipeline/`:
- `upstream-build-id` — numeric Build ID
- `upstream-source-sha` — Build.sourceVersion
- `upstream-source-branch` — Build.sourceBranch
- `upstream-status` — translated BuildResult (succeeded/...)
- `upstream-definition` — pipeline name
- `upstream-artifacts.json` — artifact INDEX (bytes NOT downloaded)
- `error.txt` — present only on REST failure
Front-matter surface (all optional):
execution-context:
pipeline:
enabled: true # default when on.pipeline is configured
Trust boundary: SYSTEM_ACCESSTOKEN is mapped only into the precompute
step's env block (never the agent step); used as bearer for the
Build REST API. Never written to disk, never logged. Build.TriggeredBy.*
identifiers are validated for shape (numeric build id, GUID project id)
before any REST call. Bundle uses the same soft-fail posture as the
PR contributor — REST failure writes error.txt + failure-fragment
that tells the agent NOT to invent an upstream-build status.
Runtime gate: `eq(variables['Build.Reason'], 'ResourceTrigger')`.
Wiring:
- New `PipelineContextContributor` in src/compile/extensions/exec_context/pipeline.rs
- `Contributor::Pipeline` variant in contributor.rs
- `pipeline_contributor_will_activate` helper in mod.rs (OR'd into
`any_contributor_active` aggregate; pub-re-exported via
extensions/mod.rs for collect_extensions)
- `EXEC_CONTEXT_PIPELINE_PATH` + `exec_context_pipeline_active: bool`
on AdoScriptExtension; OR'd into the install/download gate
- `PipelineContextConfig` on ExecutionContextConfig
- New TS bundle scripts/ado-script/src/exec-context-pipeline/
(with __tests__/index.test.ts; 12 vitest cases incl. trust-boundary)
- New scripts/ado-script/src/shared/build.ts + __tests__/build.test.ts
(5 vitest cases incl. retry + non-transient rethrow)
- Build.TriggeredBy.* (BuildId/DefinitionId/DefinitionName/ProjectID)
added to ADO macro allowlist in ir/env.rs
Validation:
- cargo build / cargo test (2030 Rust tests, 0 failures) / clippy clean
- npm typecheck / test (316 tests) / build / smoke (6 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stage 3 of the execution-context contributor build-out (plan.md).
Adds the `ci-push` contributor: stages "since last green build on
this branch" diff context for non-PR push builds. Default-OFF
(opt-in via `ci-push.enabled: true`) because the helper does
ADO REST + git fetch deepening that adds startup latency.
Runtime gate: `or(eq(Build.Reason, IndividualCI), eq(Build.Reason, BatchedCI))`.
Artefacts staged under `aw-context/ci-push/`:
- `current-sha` — Build.SourceVersion
- `previous-sha` — SHA of the last successful build of this
pipeline on this branch (REST lookup)
- `base.sha` — git merge-base(previous, current)
- `commits.txt` — `git log previous..current --oneline`
- `changed-files.txt` — `git diff --name-status previous..current`
- `error.txt` — present only on failure
Failure paths (each gets the failure fragment that tells the agent
NOT to claim "diff is empty"):
- No previous successful build (first push / all prior failed / pruned)
- Depth-budget exhausted during `git fetch` deepening
- REST call to listLastSuccessfulBuildOnBranch failed
Adds `listLastSuccessfulBuildOnBranch` helper to shared/build.ts
(reused by Stage 5 schedule contributor).
Trust boundary: SYSTEM_ACCESSTOKEN is the bearer for both the REST
lookup AND the git fetch deepening (passed via bearerEnv from
shared/git.ts → spawned git child via GIT_CONFIG_* env vars). Bearer
never reaches argv, never written to .git/config, never visible to
the agent process. Bash allow-list grows by the seven read-only git
commands (same as PR contributor).
Wiring:
- New CiPushContextContributor in src/compile/extensions/exec_context/ci_push.rs
- Contributor::CiPush enum variant
- ci_push_contributor_will_activate helper (OR'd into aggregate flag)
- EXEC_CONTEXT_CI_PUSH_PATH + exec_context_ci_push_active on AdoScriptExtension
- CiPushContextConfig (#[serde(rename = "ci-push")]) on ExecutionContextConfig
- New TS bundle scripts/ado-script/src/exec-context-ci-push/
(with __tests__/index.test.ts; 13 vitest cases incl. trust-boundary)
- Build/clean/smoke/.gitignore/release.yml all updated
Validation:
- cargo build / cargo test (2035 Rust tests, 0 failures) / clippy clean
- npm typecheck / test (329 TS tests) / build / smoke (6 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ose boundary) Stage 4 of the execution-context contributor build-out (plan.md). Adds the `workitem` contributor in PR-linked mode only (commit-scrape and parameter-driven modes are deferred follow-ups). Activates whenever the PR contributor activates and `workitem.enabled` is not false. **This is the first contributor that crosses an untrusted-prose boundary** — WI descriptions, acceptance criteria, repro steps, and comments are user-authored. The bundle wraps every prose body with `<<<AW-UNTRUSTED:source:AW-UNTRUSTED>>>` sentinel markers via the new `shared/untrusted.ts::wrapAgentReadableUntrusted` helper, and the prompt fragment ONLY interpolates short structured fields (id / title / type / state). Long prose stays in files. Also introduces two new shared modules (used by this contributor today; expected to be reused by future prose contributors): - `shared/wit.ts` — work-item REST helpers (listPullRequestWorkItems, getWorkItem, getWorkItemComments, summariseRelations) - `shared/untrusted.ts` — sentinel wrapper + htmlToPlainText readability pass Artefacts staged under `aw-context/workitem/`: - `ids.txt` — newline-delimited linked WI ids - `<id>/summary.json` — id/type/title/state/area/iter/assignee/tags - `<id>/description.md` — sentinel-wrapped description - `<id>/acceptance.md` — sentinel-wrapped acceptance criteria - `<id>/repro.md` — sentinel-wrapped repro steps (Bug) - `<id>/comments.json` — sentinel-wrapped per-comment - `<id>/links.json` — relations summary - `<id>/attachments.json` — attachment metadata (bytes NOT downloaded) - `truncated.txt` — overflow when linked count > max-items - `errors.txt` — per-id fetch failures - `error.txt` — total-failure (all fetches failed) Caps (configurable): - `workitem.max-items` (default 5) - `workitem.max-body-kb` (default 32 KB) Three failure-fragment modes (matching the plan): - No linked WIs → informational (not error) - Partial fetch failure → list per-id reasons; continue with successes - All fetches failed → tells agent to `report_incomplete` Trust boundary: SYSTEM_ACCESSTOKEN bearer mapped only into the prepare step's env block; never to the agent step's env. The sanctioned-displayName allow-list in compiler_tests.rs gains "Stage workitem execution context (aw-context/workitem/*)". Wiring: - New WorkitemContextContributor + Contributor::Workitem variant - workitem_contributor_will_activate helper (tracks PR-contributor activation per plan's contract — pr.enabled: false suppresses workitem too) - EXEC_CONTEXT_WORKITEM_PATH + exec_context_workitem_active on AdoScriptExtension; OR'd into install/download gate - WorkitemContextConfig (#[serde(rename=...)] for max-items / max-body-kb) on ExecutionContextConfig - New TS bundle scripts/ado-script/src/exec-context-workitem/ (with __tests__/index.test.ts; 12 vitest cases incl. sentinel-wrap + trust-boundary + cap enforcement) - New scripts/ado-script/src/shared/wit.ts + untrusted.ts + per-module __tests__ (16 new vitest cases) - docs/execution-context.md gains a dedicated "Untrusted-content boundary" section with Stage-2 detection guidance Validation: - cargo build / cargo test (2041 Rust tests, 0 failures) / clippy clean - npm typecheck / test (362 TS tests) / build / smoke (6 tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stage 5 of the execution-context contributor build-out (plan.md). Adds the `schedule` contributor: stages "since last successful run on this branch" diff context for scheduled builds. Default-OFF (opt-in via `schedule.enabled: true`) AND requires `on.schedule` to be declared. Reuses shared/build.ts::listLastSuccessfulBuildOnBranch (Stage 2) + shared/git.ts deepening (Stage 0), so the bundle is a thin variation on exec-context-ci-push. Adds `previous-run-time` (ISO-8601, from Build.finishTime) as the only field unique to scheduled context. Runtime gate: `eq(variables['Build.Reason'], 'Schedule')`. Artefacts staged under `aw-context/schedule/`: - `current-sha` — Build.SourceVersion - `previous-run-sha` — SHA of the last successful scheduled run - `previous-run-time` — ISO-8601 finishTime of that run (when available) - `commits.txt` — `git log previous..current --oneline` - `changed-files.txt` — `git diff --name-status previous..current` - `error.txt` — present only on failure Trust boundary: identical to ci-push — SYSTEM_ACCESSTOKEN is the bearer for both REST + git fetch, mapped only into this step's env. Bash allow-list grows by the same seven read-only git commands. Wiring follows the established pattern: - New ScheduleContextContributor in exec_context/schedule.rs - Contributor::Schedule enum variant - schedule_contributor_will_activate helper (OR'd into aggregate flag) - EXEC_CONTEXT_SCHEDULE_PATH + exec_context_schedule_active on AdoScriptExtension; OR'd into install/download gate - ScheduleContextConfig on ExecutionContextConfig - New TS bundle scripts/ado-script/src/exec-context-schedule/ (with __tests__/index.test.ts; 7 vitest cases) Validation: - cargo build / cargo test (2046 Rust tests, 0 failures) / clippy clean - npm typecheck / test (369 TS tests) / build / smoke (6 tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stage 6 of the execution-context contributor build-out (plan.md).
Adds the `pr.checks` extension of the PR contributor: enumerates
Build Validation runs on the PR and stages failing/succeeded
arrays so a remediation agent can read failure logs and propose
fixes. Default-OFF (opt-in via `pr.checks.enabled: true`) AND
requires the PR contributor to activate.
Adds `listBuildsForPullRequest` helper to shared/build.ts —
queries the Build REST API by PR ref (`refs/pull/<id>/merge`).
Artefacts staged under `aw-context/pr/checks/`:
- `failing.json` — Build Validation runs with result != Succeeded
(failed / partiallySucceeded / canceled)
- `succeeded.json` — runs with result == Succeeded
- `error.txt` — REST failure
Trust boundary: SYSTEM_ACCESSTOKEN bearer for REST; same posture
as the other PR contributors. The sanctioned-displayName allow-list
in compiler_tests.rs gains
"Stage PR-checks execution context (aw-context/pr/checks/*)".
Implementation note: pr.checks is logically an extension of the
PR contributor but is implemented as a separate Contributor variant
(`Contributor::PrChecks`) for emit-cleanliness — the YAML gets a
distinct step with its own runtime gate / displayName, and the
`should_activate` chain (PR-contributor-enabled AND opt-in)
gives precise control.
Wiring:
- New PrChecksContextContributor in exec_context/pr_checks.rs
- Contributor::PrChecks enum variant
- pr_checks_contributor_will_activate helper (tracks PR activation)
- EXEC_CONTEXT_PR_CHECKS_PATH + exec_context_pr_checks_active on
AdoScriptExtension; OR'd into install/download gate
- PrChecksContextConfig nested in PrContextConfig (so the YAML
surface stays `pr.checks.enabled` rather than a top-level key)
- New TS bundle scripts/ado-script/src/exec-context-pr-checks/
(with __tests__/index.test.ts; 8 vitest cases)
Validation:
- cargo build / cargo test (2051 Rust tests, 0 failures) / clippy clean
- npm typecheck / test (377 TS tests) / build / smoke (6 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stage 7 of the execution-context contributor build-out (plan.md).
Adds the `repo` contributor: stages repository identity info
(branch, SHA, last release tag, commits-since-tag) for any agent
that wants `what repo am I in` framing without restating it in
every markdown body. Default-OFF (opt-in via `repo.enabled: true`)
to avoid prompt-clutter regression.
Pure git, no REST, no bearer. Always-on capability (no Build.Reason
gate) — the contributor's content is useful on any build reason.
Artefacts staged under `aw-context/repo/`:
- `branch` — Build.SourceBranchName (refs/heads/ stripped)
- `sha` — Build.SourceVersion
- `last-release-tag` — `git describe --tags --abbrev=0` (empty
when no tags reachable)
- `commits-since-tag.txt` — `git log <tag>..HEAD --oneline`
- `conventions.json` — presence flags + first 50 lines of each
found of CODEOWNERS / CONTRIBUTING.md /
.editorconfig / AGENTS.md. ONLY staged
when `repo.conventions: true`.
Trust boundary: pure git against local workspace. No SYSTEM_ACCESSTOKEN
projected; no REST calls. Bash allow-list grows by 4 read-only git
commands (git / git log / git rev-parse / git describe).
Note on plan's overlap discussion: this iteration stages `sha`
independently (does not demote pr/ci-push current-sha to symlinks).
That refactor is deferred to a follow-up — the overlap is harmless
(same value written to two paths); only sentinel cleanup at the
aggregation layer would change the layout.
Wiring follows the established pattern (new RepoContextContributor +
Contributor::Repo + repo_contributor_will_activate helper +
EXEC_CONTEXT_REPO_PATH + exec_context_repo_active on AdoScriptExtension +
RepoContextConfig on ExecutionContextConfig).
Bundle is small (~8 KB; no SDK needed — pure git via shared/git.ts).
Validation:
- cargo build / cargo test (2055 Rust tests, 0 failures) / clippy clean
- npm typecheck / test (382 TS tests) / build
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-boundary parity
Final stage of the execution-context contributor build-out (plan.md).
Three cross-cutting cleanups now that all seven contributors are in
tree:
1. **Stable prompt-fragment ordering.** `ExecContextExtension::contributors()`
now returns contributors in a canonical order:
repo → pr → pipeline → ci-push → schedule → workitem → pr.checks → manual
This is the order prepare-step display names appear in the
emitted YAML and (consequently) the order prompt fragments are
appended to the agent prompt file. Rationale: identity context
first (repo), then trigger-specific diff/build context, then
linked-WI context (depends on PR context), then PR-checks
(depends on workitem framing), then manual (free-form parameter
snapshot). New test `prepare_step_ordering_is_stable_and_canonical`
pins the order so a future contributor author cannot silently
reorder.
2. **Bash-allow-list dedup audit.** Five contributors (pr, ci-push,
schedule, repo) declare overlapping read-only `git` commands.
`ExecContextExtension::bash_commands()` already deduped via
`out.sort(); out.dedup();` — but there was no test trapping a
future regression where a contributor's commands get added
without dedup. New test `bash_commands_are_deduped_across_active_contributors`
activates pr + ci-push + workitem + schedule + repo simultaneously
and asserts the aggregate output has no duplicates.
3. **Trust-boundary test parity.** The sanctioned-displayName
allow-list in `test_execution_context_pr_does_not_leak_system_accesstoken`
now lists every contributor that legitimately maps
`SYSTEM_ACCESSTOKEN` into its prepare step's env block:
- Stage PR execution context (aw-context/pr/*) [pr]
- Stage pipeline execution context (aw-context/pipeline/*) [pipeline]
- Stage ci-push execution context (aw-context/ci-push/*) [ci-push]
- Stage workitem execution context (aw-context/workitem/*) [workitem]
- Stage schedule execution context (aw-context/schedule/*) [schedule]
- Stage PR-checks execution context (aw-context/pr/checks/*) [pr.checks]
- Execute safe outputs (Stage 3) [SafeOutputs]
- Resolve synthetic PR context [synthPr]
Each entry's comment explains why that step legitimately holds
the bearer. Adding a new bearer-holding step to the codebase
without adding it here will trip the test.
Validation:
- cargo build / cargo test (2057 Rust tests, 0 failures) / clippy clean
- npm test (382 TS tests) / smoke (6 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good to merge — well-engineered contribution with strong trust-boundary discipline, comprehensive tests, and no blocking issues. A few minor nits below. Findings
|
Addresses the four suggestions from the automated Rust PR Reviewer (comment 4705527052) — all marked `minor / cosmetic`, none blocking the `Looks good to merge` summary. 1. **types.rs indentation.** The 6 new `ExecutionContextConfig` fields (manual, pipeline, ci_push, workitem, schedule, repo) and the `checks` field on `PrContextConfig` were over-indented by 4 spaces; the follow-on `impl` blocks for both structs (plus `PrChecksContextConfig`) had the same drift. Realigned to match the original `enabled` / `pr` field indent and module-level `impl` block indent. (cargo fmt would have caught this, but running it would also reformat unrelated files — manual fix only.) 2. **Redundant master-switch check** in `workitem_contributor_will_activate_with_cfg` (mod.rs). `pr_contributor_will_activate_with_cfg` already enforces `cfg.is_enabled()` internally, so the explicit re-check was dead code. Removed it and updated the comment to note why we don't re-check. 3. **MAINTENANCE comment mismatch** in `manual.rs::should_activate`. The old comment said the function must stay in lock-step with `manual_contributor_will_activate`, but the two deliberately diverge on the master-switch check (the predicate enforces it, the contributor delegates to the outer `declarations()` guard). Rewrote the comment to clarify that they must agree only on the contributor-local conditions (parameter list + per-contributor enabled flag) and to explain where the master switch is enforced. 4. **workitem bundle infra-failure path** now appends a failure fragment to the agent prompt before returning 1 from main(), matching the posture of every other contributor (manual, pipeline, ci-push, schedule, pr-checks, repo). Wrapped in a defensive try/catch since the same infra issue that broke mkdirSync may also break the prompt write — best-effort only, the underlying infra error takes precedence. Validation: - cargo build / cargo test (2063 Rust tests, 0 failures) / clippy clean - npm typecheck / test (382 TS tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: looks good — solid architecture, well-tested; two items worth addressing before merge. Findings🔒 Security Concerns
This creates a premature "end of untrusted region" in the staged The code acknowledges this and says the source label makes the outer pair "still distinctive" — that's true, but it silently requires Stage-2 detection to implement source-label-aware nesting rather than simple open/close pair scanning. That requirement isn't stated in the module's API contract or in Recommendation: Either (a) add an explicit note to
|
Addresses follow-up review feedback on PR #1019: the `wrapAgentReadableUntrusted` helper used to validate the `source` label but pass the `body` through verbatim. An adversarial WI author (anyone with WI write access can edit prose bodies) could embed the literal closing sentinel `:AW-UNTRUSTED>>>` in their description, forging a fake `end of untrusted region` that naive open/close pair scanning by Stage-2 detection would honour — allowing the attacker to make subsequent body content appear to lie outside the boundary. Fix: escape any literal sentinel-marker substrings in the body before wrapping. Both the prefix (`<<<AW-UNTRUSTED:`) and the suffix (`:AW-UNTRUSTED>>>`) are substituted with their `-ESCAPED` variants. The escape is one-way (no round-trip back to the original text) — the body is read-only by the agent, so structural unambiguity matters more than byte fidelity. The forged-sentinel attempt is still visible in the staged file (as the `-ESCAPED` marker), giving Stage-2 detection a smuggling- attempt signal it can flag. Changes: - `scripts/ado-script/src/shared/untrusted.ts`: add `escapeSentinelMarkers` private helper + constants for the escaped variants; call it from `wrapAgentReadableUntrusted` before composing the wrapped output. - `scripts/ado-script/src/shared/__tests__/untrusted.test.ts`: add 3 new tests covering forged-close, forged-open, and repeated-marker injection. Update the existing "preserves body verbatim" test to clarify it only applies to non-sentinel content. - `scripts/ado-script/src/exec-context-workitem/index.ts`: extend the prompt fragment's UNTRUSTED CONTENT BOUNDARY note so the agent knows that an `-ESCAPED` substring inside a region indicates a neutralised smuggling attempt and warrants extra suspicion. - `docs/execution-context.md`: document the escape behaviour in the workitem contributor's "Untrusted-content boundary" section (both the mechanism and Stage-2 detection guidance for the `-ESCAPED` smuggling signal). Validation: - npm typecheck + test (385 TS tests passed, +3 new sentinel-injection tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation — trust boundary design is particularly well-executed. Two findings worth addressing before merge. Findings🐛 Bugs / Logic Issues
✅ What Looks Good
|
…p_typed Addresses follow-up review feedback on PR #1019: the `workitem`, `pipeline`, `ci_push`, `schedule`, `pr_checks`, and `repo` contributors all unconditionally returned `Ok(Some(Step::Bash(...)))` from `prepare_step_typed`, unlike `manual.rs` which has an explicit early-return guard. The outer `ExecContextExtension::declarations` loop checks `should_activate` before calling `prepare_step_typed`, so this wasn't a runtime bug — but a future test or tool that called `prepare_step_typed` directly (as the existing `hostile_parameter_name_rejected_by_contributor` test does in manual.rs) would silently get a live step instead of `None`. For the bearer-holding contributors (workitem in particular — fetches linked-WI prose via REST) this would mean a future test could accidentally emit a step with a live `SYSTEM_ACCESSTOKEN` into the YAML. Fix: add `if !self.should_activate(ctx) { return Ok(None); }` at the top of `prepare_step_typed` for all six contributors. This mirrors the manual.rs pattern uniformly. Bonus: the `_ctx` parameter is now actually used, so the parameter name drops the underscore prefix. New test `workitem::tests::prepare_step_returns_none_when_inactive` exercises the guard across all three inactive paths for the highest-consequence contributor (no on.pr, workitem.enabled: false, pr_contributor disabled). Other contributors' guards are implicitly covered by their `should_activate` tests + the canonical-ordering test in `mod.rs::tests`. Validation: - cargo build / cargo test (2064 Rust tests, 0 failures) / clippy clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — well-structured, strong trust-boundary discipline, and excellent test coverage. One genuine bug in a defensive guard, plus a minor test coverage gap. Findings🐛 Bugs / Logic Issues
✅ What Looks Good
|
… other contributors Addresses follow-up review feedback on PR #1019: `manual.rs`'s `prepare_step_typed` guard was `self.parameter_names.is_empty()` — the sub-check covering only the "no parameters declared" path. This meant a direct call to `prepare_step_typed` with `config.enabled == Some(false)` AND non-empty parameters would still emit a live step, bypassing the explicit opt-out. The manual contributor carries no SYSTEM_ACCESSTOKEN so the security blast radius is low — but the pattern divergence undermines the defensive guarantee the comment promises and breaks consistency with the other five contributors (workitem, pipeline, ci_push, schedule, pr_checks, repo) which all use the `!should_activate(ctx)` form. Fix: replace the sub-check with `!self.should_activate(ctx)`, matching every other contributor. The `ctx` parameter loses its underscore prefix since it's now used. Comment rewritten to explain why the full predicate matters. New test `prepare_step_none_when_explicitly_disabled` pins the case the old guard missed: `enabled: Some(false)` plus non-empty parameters MUST return `Ok(None)`. Validation: - cargo test (2065 Rust tests, 0 failures; 10 manual tests, was 9) / clippy clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — strong security posture and comprehensive test coverage. Three minor findings below. Findings
|
Summary
Implements the full execution-context contributor build-out from
plan.md, adding 7 new trigger-context contributors plus thekeystone
shared/refactor that makes them reusable. Closes outthe "Future trigger contributors (pipeline-completion, schedule,
manual)" placeholder originally noted in
docs/execution-context.md.Why
Issue #860 introduced the execution-context plugin with a single
prcontributor. The plan called out 6 future contributors —pipeline-completion, ci-push, schedule, manual, workitem, and a
pr.checks extension — that would slot in via the same internal
ContextContributortrait. This PR ships all of them, plus arepocontributor for always-on identity context, plus theStage-0 refactor that prevents fragmentation as more
contributors land.
What lands
9 stacked commits, in order:
1971d40ashared/refactor —git mv4 helpers (no functional change)94267719manualcontributor (parameter snapshot, no bearer)f70b13bapipelinecontributor +shared/build.ts456fb957ci-pushcontributor (default OFF)7db94404workitemcontributor +shared/wit.ts+shared/untrusted.tse19b081dschedulecontributor (default OFF)7b80cac4pr.checksextension (Build Validation triage)d27355f9repocontributor (default OFF, pure git)bc859464Per-commit messages explain the trust boundary, runtime gate,
staged artefacts, and wiring touchpoints for each contributor.
Activation summary
pron.prconfiguredmanualparameters:declaredpipelineon.pipelineconfiguredci-pushci-push.enabled: true(IndividualCI / BatchedCI)workitempr(PR-linked mode only)scheduleon.scheduledeclared ANDschedule.enabled: truepr.checksprANDpr.checks.enabled: truereporepo.enabled: trueDefaults follow the plan's contract — opt-in for contributors that
incur ADO REST/git-fetch cost or add prompt clutter; on-by-default
for the cheap / contextually-obvious ones.
Trust boundary
SYSTEM_ACCESSTOKENis mapped only into each prepare step'senv:block; never into the agent step's env. Stage 8 adds thefull sanctioned-displayName allow-list in
tests/compiler_tests.rs::test_execution_context_pr_does_not_leak_system_accesstoken,one entry per bearer-holding step with rationale.
workitemcontributor is the first contributor that crossesan untrusted-prose boundary. WI bodies / comments are wrapped
via
shared/untrusted.ts::wrapAgentReadableUntrustedwith a<<<AW-UNTRUSTED:source:AW-UNTRUSTED>>>sentinel. Stage-2detection guidance is documented in
docs/execution-context.md(new Untrusted-content boundarysection).
manualare JSON-serialised(escape-safe) before disk write and sanitised via
validate.sanitizeForPromptbefore any prompt interpolation.Parameter NAMES are validated as ADO identifiers upstream AND
re-checked at emit time by the contributor.
Bash allow-list
Read-only
gitcommands are added to the agent's bash allow-listonly when a contributor that needs them activates. Dedup is
enforced and tested
(
bash_commands_are_deduped_across_active_contributors).prgit, git diff, git log, git show, git status, git rev-parse, git symbolic-refci-pushschedulerepogit, git log, git rev-parse, git describecat/jqPrompt-fragment ordering (Stage 8)
When multiple contributors activate, fragments are appended to the
agent prompt in a canonical order:
repo → pr → pipeline → ci-push → schedule → workitem → pr.checks → manualIdentity first; trigger-specific diff/build context next; linked-WI
context after PR context is established; PR-checks after workitem;
manual parameter snapshot last (it doesn't fit the diff/identity
narrative). Test
prepare_step_ordering_is_stable_and_canonicalpins the order.
Release plumbing
scripts/ado-script/package.json(build / clean / test:smoke),scripts/ado-script/.gitignore, and.github/workflows/release.ymlare all updated for the 7 new nccbundle outputs.
ado-script.zipnow contains 8 bundles total.Deferred follow-ups (not in this PR)
workitemcommit-scrape mode (activates on ci-push when commitmessages reference
#NNN/AB#NNN)workitemparameter-driven mode (workitem-idruntime parameter)repo/pr/ci-pushcurrent-shaoverlap resolution(currently each stages it independently; could demote to
aw-context/repo/shawith references)workitembodies if the minimalhtmlToPlainTextreadability pass proves insufficient inpractice (
turndownadds ~100 KB to the bundle)Test plan
All gates pass at HEAD of every commit and at branch tip:
cargo build— clean (Windows MSVC debug)cargo test— 2057 tests passed, 0 failed, 1 ignored across17 test binaries
cargo clippy --all-targets --all-features— cleannpm run codegen && npm run typecheck— cleannpm test(vitest) — 382 tests passed across 44 test filesnpm run build— all 8 ncc bundles built successfullynpm run test:smoke— 6 end-to-end smoke tests passedNew test coverage added per stage:
manual.rs::tests) + 16 vitest casespipeline.rs) + 12 vitest cases + 5shared/buildREST testsci_push.rs) + 13 vitest casesworkitem.rs) + 12 vitest cases + 11shared/wit.ts+ 11shared/untrusted.tstestsschedule.rs) + 7 vitest casespr_checks.rs) + 8 vitest casesrepo.rs) + 5 vitest casesmaster switch) + sanctioned-displayName allow-list expanded
to cover every bearer-holding step
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com