Skip to content

fix(safeoutputs): stop dropping GitHub issue tool configs in Stage 3 - #1804

Merged
jamesadevine merged 1 commit into
mainfrom
fix/stage3-tool-config-staged-key
Aug 3, 2026
Merged

fix(safeoutputs): stop dropping GitHub issue tool configs in Stage 3#1804
jamesadevine merged 1 commit into
mainfrom
fix/stage3-tool-config-staged-key

Conversation

@jamesadevine

Copy link
Copy Markdown
Collaborator

What

ExecutionContext::get_tool_config stripped the compiler-injected require-approval key but not staged, which Stage 3 also injects into every tool config:

  • src/main.rs — the --source path
  • src/compile/custom_tools.rs — the compiler-generated --resolved-config, which is what production uses

CreateGithubIssueConfig and SetGithubIssueTypeConfig are the only safe-output configs declared #[serde(deny_unknown_fields)], so deserialization failed. The error was then swallowed:

let mut config: T = value
    .and_then(|v| serde_json::from_value(v).ok())   // error dropped
    .unwrap_or_default();                           // config wiped

The operator config was silently replaced with Default::default().

Impact

Config Actual behaviour before this fix
target-repo Ignored — Stage 3 failed outright on non-GitHub-backed ADO builds
title-prefix Never applied
labels, assignees Static entries never applied
allowed-labels Emptied → default-deny rejected every agent label
require-temporary-id Never enforced
max Budget override ignored
set-github-issue-type.allowed Never enforced — empty is default-allow, so this failed open

The last row is the security-relevant one: the label allowlist failed closed, but the issue-type allowlist failed open.

Reproduction

Using the exact config shape the compiler emits:

echo '{"name":"x","toolConfigs":{"create-github-issue":{"target-repo":"octo/scratch","staged":false}}}' > resolved.json
ADO_AW_GITHUB_TOKEN=ghp_bogus ado-aw execute --resolved-config resolved.json --safe-output-dir out \
  --ado-org-url https://dev.azure.com/x/ --ado-project P

Before: create-github-issue - ✗ - target-repo is required when the Azure DevOps pipeline source is not GitHub

After: create-github-issue - ✗ - Failed to file GitHub issue (HTTP 401 Unauthorized) — i.e. the config is honoured and the call reaches GitHub (401 is just the bogus token).

Removing only "staged": false from the config also fixed it before this change, which isolates the cause.

The fix

  1. Strip staged alongside require-approval.
  2. Log a warn! instead of silently defaulting, so a future config-shape mismatch is visible rather than presenting as a mysterious runtime failure.

Why no existing test caught it

The wiremock tests construct an ExecutionContext directly with a tool_configs map that has no staged key — a shape that never occurs in production.

The two added regression tests assert an operator config survives both injected keys, and are mutation-verified: reverting the one-line object.remove("staged") turns both red.

Testing

  • cargo test --bin ado-aw — 2828 passed
  • cargo clippy --bin ado-aw — clean
  • No new cargo fmt drift (4 pre-existing diffs in this file are unchanged)

Found while adding deterministic executor-e2e coverage for these tools, which follows in a stacked PR.

`ExecutionContext::get_tool_config` stripped the compiler-injected
`require-approval` key but not `staged`, which Stage 3 also injects into
every tool config (`main.rs` for the `--source` path,
`compile/custom_tools.rs` for the `--resolved-config` path production
actually uses).

`CreateGithubIssueConfig` and `SetGithubIssueTypeConfig` are the only
safe-output configs declared `deny_unknown_fields`, so deserialization
failed — and the error was swallowed by `.ok().unwrap_or_default()`,
silently replacing the operator config with `Default::default()`.

Observable effects:

* `target-repo` ignored, so Stage 3 failed outright on non-GitHub-backed
  ADO builds with "target-repo is required when the Azure DevOps pipeline
  source is not GitHub"
* `title-prefix` never applied; static `labels`/`assignees` dropped
* `allowed-labels` emptied, so default-deny rejected *every* agent label
* `require-temporary-id` never enforced; `max` budget override ignored
* `set-github-issue-type.allowed` never gated anything — an empty list is
  default-allow, so this one failed OPEN

Strip `staged` alongside `require-approval`, and log a warning instead of
silently defaulting so a future config-shape mismatch is visible rather
than presenting as a mysterious runtime failure.

The existing wiremock tests could not catch this: they build an
`ExecutionContext` directly with a `tool_configs` map that has no
`staged` key, i.e. a shape that never occurs in production. The added
regression tests assert an operator config survives both injected keys.

Found while adding deterministic executor-e2e coverage for these tools.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cefea537-5177-4dfd-8849-bcc2caff2845
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@jamesadevine
jamesadevine merged commit f660667 into main Aug 3, 2026
6 checks passed
@jamesadevine
jamesadevine deleted the fix/stage3-tool-config-staged-key branch August 3, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant