fix: report-incomplete fails pipeline, percent-encode user_id, stage-1 status validation, merge_strategy validation, dead code removal#141
Conversation
Implements the remaining safe output features from GitHub issues #72-#79: - add-pr-comment (#72): Create comment threads on Azure DevOps pull requests with support for general and file-specific inline comments - link-work-items (#73): Create relationships between work items (parent/child, related, predecessor/successor, duplicate) - queue-build (#74): Trigger Azure DevOps pipeline/build runs with optional branch and template parameters - create-git-tag (#75): Create annotated git tags on commits in ADO repositories - add-build-tag (#76): Add tags to Azure DevOps builds for classification - create-branch (#77): Create new branches without immediately creating a PR - update-pr (#78): Update PR metadata (reviewers, labels, auto-complete, vote, description) - upload-attachment (#79): Upload file attachments to work items Each tool includes: - Params struct with validation - Config struct for front matter configuration - Executor with full ADO REST API implementation - Sanitize impl for security - Comprehensive unit tests All 8 tools are registered in the MCP server, Stage 2 executor dispatch, budget system, and write-permissions validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Needs changes — solid foundation with good patterns throughout, but two correctness bugs and one meaningful security gap. Findings🐛 Bugs / Logic Issues1. // Line ~170
"{}/{}/_apis/git/repositories/{}/refs?filter=heads/main&api-version=7.1"The
2. let reviewer_url = format!(
"{}/{}/pullRequests/{}/reviewers/{}?api-version=7.1",
base_url, encoded_repo, self.pull_request_id,
utf8_percent_encode(reviewer, PATH_SEGMENT), // ← reviewer is an email
);
```
The `PUT .../reviewers/{reviewerId}` ADO endpoint expects a user **GUID/descriptor** as `reviewerId`, not an email address. Passing an email will consistently return a `404` or `400`. In practice the `add-reviewers` operation will always silently fail — every reviewer ends up in the `failed` list and the operation returns partial-success.
The fix requires an identity lookup step first:
```
GET (vssps.dev.azure.com/redacted)Then use the returned 🔒 Security Concerns3. The PR description highlights "vote restrictions to prevent unauthorized approvals," but looking at the actual default: impl Default for UpdatePrConfig {
fn default() -> Self {
Self {
allowed_votes: Vec::new(), // ← empty = all votes allowed
...
}
}
}And the enforcement: if !config.allowed_votes.is_empty() && !config.allowed_votes.contains(&vote_str.to_string()) {An empty Suggested fix: flip the default so that if config.allowed_votes.is_empty() {
return Ok(ExecutionResult::failure(
"vote operation requires allowed-votes to be configured"
));
}Or at minimum restrict
|
Bug fixes: - create-git-tag: resolve HEAD via repository's defaultBranch instead of hardcoding 'heads/main' (works with master, develop, etc.) - update-pr: resolve reviewer emails to GUIDs via VSSPS identity API before adding to PR (ADO reviewers endpoint requires user ID, not email) Security: - update-pr: require 'allowed-votes' to be explicitly configured for vote operation — empty list now rejects all votes to prevent accidental auto-approve by agents Improvements: - update-pr: make auto-complete merge options configurable via front matter (delete-source-branch, merge-strategy) instead of hardcoding squash+delete - add-pr-comment: validate file_path in Validate::validate() at Stage 1 (was only validated at Stage 2, giving no feedback to agent) - queue-build: sanitize template parameter keys and values to prevent ##vso[] command injection in build logs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: 8 new tools with solid overall structure, but two bugs in Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
Adds 4 new safe output tools to align with gh-aw's PR review capabilities: - submit-pr-review: Submit PR review with decision (approve, request-changes, comment) and optional body/rationale text. Requires allowed-events config. - reply-to-pr-review-comment: Reply to existing review comment threads - resolve-pr-review-thread: Resolve/reactivate review threads (fixed, wont-fix, closed, by-design, active) - report-incomplete: Signal task could not complete due to infrastructure failure Enhancements to existing tools: - add-pr-comment: Add start_line param for multi-line code comments (gh-aw parity) - link-work-items: Set DEFAULT_MAX=5 (matches gh-aw link-sub-issue default) - queue-build: Set DEFAULT_MAX=3 (prevents pipeline queue flooding) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bug fixes: - update-pr: derive connectiondata URL from org_url instead of hardcoding dev.azure.com — supports vanity domains and national cloud endpoints - update-pr: derive VSSPS identity URL from org_url instead of hardcoding vssps.dev.azure.com — same environment portability fix - submit-pr-review: same connectiondata URL fix (derived from org_url) Security: - upload-attachment: add canonicalize() + prefix check after resolving file path to prevent symlink escape attacks (symlinks within workspace could previously be followed to read arbitrary files outside source_directory) Improvements: - add-pr-comment: sanitize structural fields (repository, status, file_path) with control-char stripping for defense-in-depth consistency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/rust-review |
|
✅ Rust PR Reviewer completed successfully! |
🔍 Rust PR ReviewSummary: Previous review findings are resolved — one lingering bug in Findings🐛 Bugs / Logic Issues
|
- queue-build: drop equality arm from release/* wildcard matching so bare 'release' no longer matches 'release/*' — only 'release/...' does - update-pr: add warn! log when VSSPS URL derivation produces no change (surfaces the issue for visualstudio.com-hosted orgs at runtime) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Largely solid — good security model, consistent patterns — but one clear bug and a few concerns worth addressing before merge. Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
Bug fix: - add-pr-comment: fix WontFix status mapped to 6 (should be 3). Unify status strings to kebab-case (active, fixed, wont-fix, closed, by-design) with CamelCase accepted for backwards compatibility. Security: - update-pr: reject add-reviewers for non-dev.azure.com org URLs instead of silently proceeding with a malformed VSSPS URL (*.visualstudio.com orgs) Improvements: - queue-build: sanitize branch field for defense-in-depth consistency - update-pr: document allow-list semantics asymmetry (operations=permissive, votes=secure) in UpdatePrConfig doc comment - create-branch: check config policy (allowed-repositories) before resolving repo alias, so operators see a policy error not a checkout-list error Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation with good security defaults, a couple of correctness bugs worth fixing before merge. Findings🐛 Bugs / Logic Issues
The // BUG: this `?` fires BEFORE the config policy check below it
let repo_name = if repo_alias == "self" {
...
} else {
ctx.allowed_repositories
.get(repo_alias)
.cloned()
.context(format!("Repository alias '{}' not found...", repo_alias))? // hard error
};
// This block is never reached for an unknown alias
if !config.allowed_repositories.is_empty()
&& !config.allowed_repositories.contains(&repo_alias.to_string())
{
return Ok(ExecutionResult::failure(...));
}
The VSSPS URL is re-derived (and the equality check is re-evaluated) inside the // Currently inside the for loop
let vssps_base = trimmed_org.replace("://dev.azure.com/", "://vssps.dev.azure.com/");
if vssps_base == trimmed_org {
return Ok(ExecutionResult::failure(...));
}Move this to the top of 🔒 Security Concerns
Both files define an identical
The injection check is skipped for binary files (any file where
|
Bug fixes: - create-git-tag: check config allowed-repositories before resolving repo alias (same fix as create-branch — policy error before resolution error) - update-pr: hoist VSSPS URL derivation and validation out of the per-reviewer loop (avoids repeated work, fails fast before any API calls) Refactor: - Extract resolve_repo_name() to tools/mod.rs — was duplicated identically in update_pr.rs and submit_pr_review.rs Documentation: - add-pr-comment: clarify start_line must be strictly less than line - reply-to-pr-comment: explain parentCommentId=1 targets root comment - upload-attachment: document ##vso[] scan UTF-8 boundary behavior - mcp.rs: log warning on report-incomplete write failure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid overall — the security model (config-driven allow-lists, required Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
|
Bugs / Logic Issues src/execute.rs:384–387 — report-incomplete silently succeeds instead of failing the stage This is the most significant issue. The tool's stated purpose is to "signal that the task could not be completed due to an infrastructure failure" and cause the pipeline to fail — directly analogous to gh-aw's report_incomplete. However it's handled identically to noop: "report-incomplete" => { "report-incomplete" => { 🔒 Security Concerns src/tools/submit_pr_review.rs:263, src/tools/update_pr.rs:527 — user_id from API response not percent-encoded in URL The user identity GUID returned by _apis/connectiondata is interpolated directly into the PUT URL without encoding: let vote_url = format!( src/tools/add_pr_comment.rs — status field not validated at Stage 1 AddPrCommentParams::validate() doesn't check status against VALID_STATUSES. An agent can write any value (e.g. "unknown") to the NDJSON file and it only fails at Stage 2 when status_to_int() returns None. This is inconsistent with the rest of the validation pattern (validate-at-source). A one-liner in validate() would fix it: ensure!( UpdatePrConfig::merge_strategy defaults to "squash" but accepts any string from the operator's front matter. ADO's completionOptions.mergeStrategy only accepts "squash", "noFastForward", "rebase", "rebaseMerge". An invalid value causes a runtime ADO 400 with a cryptic message. Worth validating at Default construction or adding a note in the doc comment. src/tools/upload_attachment.rs — dead code after canonicalize() The canonical.exists() check after a successful canonicalize() call is unreachable — canonicalize() already fails if the path doesn't exist: // This branch can never execute — canonicalize() would have failed above |
…ser_id, status validation, merge_strategy validation, dead code removal Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/474893c9-505a-48fb-a77e-768710027c85 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/474893c9-505a-48fb-a77e-768710027c85 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
All issues addressed in commits Bug fix — Security — Validation — Validation — Dead code — Removed the unreachable Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
/rust-review |
|
✅ Rust PR Reviewer completed successfully! |
🔍 Rust PR ReviewSummary: looks good — the five stated fixes are all correct; one misleading claim in the PR description and one overly-strict Stage 1 path check worth noting. Findings🐛 Bugs / Logic Issues
|
|
Bugs / Logic Issues src/tools/update_pr.rs:357–389 — The PR description says execute_set_auto_complete returns a descriptive failure "before touching the API", but the VALID_MERGE_STRATEGIES guard (line 389) is placed after a GET /pullRequests/{id} call (line 357). On an invalid merge-strategy config value a needless round-trip is made before the early return. This is purely wasteful (the PATCH is still blocked), but the description is misleading. Consider moving the guard to the top of execute_set_auto_complete, before any network I/O. src/tools/upload_attachment.rs:32–34 — Stage 1 path validation uses file_path.contains(".."), which rejects legitimate filenames like report..v2.pdf or v2..3/notes.md. The actual escape defence is the canonicalize() + starts_with() check at Stage 2 (line 178–190), so this is functionally safe, but Stage 1 is over-restrictive. The .git component check on lines 50–57 already uses the correct pattern (split('/').any(|c| c == ".git")); the same per-component equality check (c == "..") would be more precise here and is consistent with validate_file_path in add_pr_comment.rs. src/tools/submit_pr_review.rs:~107 — allowed-events defaults to an empty Vec, which causes a runtime failure with a helpful message. Other tools like comment-on-work-item enforce their required config field (target) at compile time with bail!. An empty allowed-events list is a misconfiguration that could be caught at cargo run -- compile time rather than deferred to Stage 2. |
…-time submit-pr-review validation Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/c36b2130-22bf-49ac-929f-0de6cada3ef6 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
All three items addressed in commit Bug — Suggestion — Suggestion — compile-time Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
/rust-review |
|
✅ Rust PR Reviewer completed successfully! |
🔍 Rust PR ReviewSummary: Good foundational work adding 11 new safe-output tools — structure is consistent throughout. Three issues need attention before merge. Findings🐛 Bugs / Logic Issues
|
|
🐛 Bugs / Logic Issues src/tools/submit_pr_review.rs:306 — "status": 4 is hardcoded for the review comment thread body. In the ADO API, status 4 = Closed/Resolved, not Active. A newly posted review comment should be 1 (Active) so reviewers can engage with it. Currently all review rationale comments are posted as pre-resolved/closed threads, defeating their purpose entirely. Fix: change "status": 4 to "status": 1. src/tools/add_pr_comment.rs:230-231 — allowed_statuses.contains(&self.status) is a case-sensitive exact match, but status_to_int() accepts both "active" and "Active". The documented canonical form is lowercase kebab-case ("active", "wont-fix"), but the test example config uses ["Active", "Closed"]. An operator who configures ["Active"] and an agent that sends "active" (the canonical form) gets a false denial at Stage 2, silently breaking a valid workflow. Fix: config.allowed_statuses.iter().any(|s| s.eq_ignore_ascii_case(&self.status)) src/tools/update_pr.rs:467 vs src/compile/common.rs:793 — The vote operation has a runtime-only allowed_votes.is_empty() guard (Stage 2 failure). Unlike submit-pr-review which has validate_submit_pr_review_events() as a compile-time check, a misconfigured update-pr with operation: vote and no allowed-votes silently compiles and only fails at Stage 2. Consider adding a validate_update_pr_votes() compile-time validator analogous to validate_submit_pr_review_events() and wiring it into both standalone.rs and onees.rs |
…pile-time update-pr vote validation Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/147051a9-a11a-4967-9c19-9fd9a2093064 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
All three items addressed in commit Bug — Bug — case-insensitive Suggestion — compile-time Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
/rust-review |
|
✅ Rust PR Reviewer completed successfully! |
…/new-safe-outputs
tokio::fs::File::write_all may buffer data internally without flushing to the filesystem. Under load (e.g., full test suite), a subsequent read_to_string could see stale data, causing write_safe_output_file_with_maximum to miss already-written entries and exceed the configured limit. Adding an explicit flush() ensures data reaches the filesystem before the function returns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. Self-approval guard for submit-pr-review and update-pr vote: Both voting paths now fetch the PR createdBy.id and block positive votes (approve, approve-with-suggestions) when the authenticated identity is the PR author. 2. ##vso[ pipeline command neutralization in core sanitize(): Added neutralize_pipeline_commands() step that wraps ##vso[ and ##[ sequences in backticks, preventing ADO from interpreting them as logging commands. This provides defense-in-depth for all text fields across all safe output tools. 3. resolve-pr-review-thread fail-closed allowed-statuses: Changed from permissive default (empty = all allowed) to fail-closed (empty = all rejected). Added compile-time validator validate_resolve_pr_thread_statuses() that requires explicit allowed-statuses config, matching the pattern of submit-pr-review allowed-events and update-pr allowed-votes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4. link-work-items: Added required target config field (reuses
CommentTarget enum from comment-on-work-item). Execution now
fails if target is not configured, preventing unrestricted
cross-project work item linking.
5. queue-build: Added ADO variable injection check for template
parameter values. Rejects values containing \$(, \${{ or \$[
syntax that could reference pipeline variables.
6. upload-attachment: Fixed path traversal via backslash by
splitting on both / and \\ in .., .git, and absolute path
checks. Added tests for backslash-based traversal.
7. create-git-tag + create-branch: Added comprehensive git ref
name validation via shared validate_git_ref_name() helper.
Rejects .., @{, ~, ^, :, ?, *, [, \\, //, .lock suffix,
leading dots in path components, and trailing dots.
8. add-build-tag: Added allow-any-build config (default: false).
When not explicitly enabled, only the current pipeline build
(BUILD_BUILDID) can be tagged.
9. update-pr: Changed auto-complete to use the agent's own
identity from _apis/connectiondata instead of the PR
createdBy.id, providing correct audit attribution.
10. report-incomplete: Added sanitize_fields() call before using
the reason field in execute.rs to prevent injection via
unsanitized agent output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…/new-safe-outputs
🔍 Rust PR ReviewSummary: Mostly looks good — the five targeted fixes are well-executed. Found two bugs worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
|
🐛 Bugs / Logic Issues src/tools/add_pr_comment.rs:185 — validate_file_path uses path.contains("..") (substring match) instead of a component-wise check. This rejects legitimate paths like "releases..notes/v1.md" or "v2..beta/file.txt" where .. appears within a path component name (not as a traversal). upload_attachment.rs correctly uses path.split(['/', '\']).any(|component| component == "..") — add_pr_comment.rs should match. Since this file_path is API-bound (not disk-read), it's a correctness/usability issue rather than a security gap, but operators will hit confusing validation failures. src/compile/common.rs — validate_resolve_pr_thread_statuses error message — The compile-time bail message lists "pending" as a valid status: Valid statuses: active, fixed, wont-fix, closed, by-design, pending src/tools/resolve_pr_thread.rs:260–280 — Repository resolution is inlined rather than using the resolve_repo_name helper added in mod.rs. The inline code also special-cases effective_repo.is_empty() as "self" (the helper doesn't), so the two paths already diverge. Better to use resolve_repo_name consistently and, if the empty-string case is intentional, handle it there. |
…ng status, use resolve_repo_name helper Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/91091a0b-42a6-4559-afa4-44d12b847a4e Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
All three items addressed in commit Bug — component-wise Bug — spurious "pending" in error messages — Removed "pending" from both the compile-time Suggestion — Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Five correctness/security issues in the new safe output tools identified in review.
Bug:
report-incompletesilently succeededexecute.rswas returningExecutionResult::success(...)forreport-incomplete, meaning pipelines where the agent signalled infrastructure failure would show green. Now parses the entry and returns a failure with the agent's reason:Security:
user_idnot percent-encoded in reviewer URLsubmit_pr_review.rsandupdate_pr.rsinterpolated the GUID from_apis/connectiondatadirectly into URLs. Now encoded viautf8_percent_encode(user_id, PATH_SEGMENT), consistent withencoded_repo/encoded_project.Suggestion:
statusvalidated at Stage 1 (add_pr_comment.rs)AddPrCommentParams::validate()was not checkingstatusagainstVALID_STATUSES, deferring failure to Stage 2. Addedensure!(status_to_int(&self.status).is_some(), ...).Suggestion:
merge_strategyvalidated against ADO-accepted values (update_pr.rs)Added
VALID_MERGE_STRATEGIES: &[&str] = &["squash", "noFastForward", "rebase", "rebaseMerge"]and a guard inexecute_set_auto_completethat returns a descriptive failure before touching the API, replacing the cryptic ADO 400.Dead code removal (
upload_attachment.rs)Removed unreachable
canonical.exists()check —canonicalize()already fails if the path doesn't exist.