Skip to content

Commit e81c570

Browse files
jamesadevineCopilotCopilot
authored
fix: report-incomplete fails pipeline, percent-encode user_id, stage-1 status validation, merge_strategy validation, dead code removal (#141)
* feat: add 8 new safe output tools 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> * fix: address PR review feedback 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> * feat: align with gh-aw — add review tools and enhancements 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> * fix: address second round of PR review feedback 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> * fix: branch wildcard matching + VSSPS URL warning - 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> * fix: status mapping, VSSPS rejection, branch sanitize, policy order 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> * fix: repo policy ordering, VSSPS loop hoist, dedup resolve_repo_name 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> * fix: address PR review feedback — report-incomplete, percent-encode user_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> * fix: improve merge_strategy tests to be non-tautological 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> * fix: merge_strategy guard before GET, per-component .. check, compile-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> * fix: review thread status 4→1, case-insensitive allowed_statuses, compile-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> * fix: flush NDJSON file after write to prevent stale reads 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> * fix: address 3 critical security issues from review 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> * fix: address 7 high-severity issues from review 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> * fix: component-wise .. check in add_pr_comment, remove spurious pending 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> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent bc5e074 commit e81c570

21 files changed

Lines changed: 7131 additions & 7 deletions

src/compile/common.rs

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,17 @@ const WRITE_REQUIRING_SAFE_OUTPUTS: &[&str] = &[
678678
"update-work-item",
679679
"create-wiki-page",
680680
"update-wiki-page",
681+
"add-pr-comment",
682+
"link-work-items",
683+
"queue-build",
684+
"create-git-tag",
685+
"add-build-tag",
686+
"create-branch",
687+
"update-pr",
688+
"upload-attachment",
689+
"submit-pr-review",
690+
"reply-to-pr-review-comment",
691+
"resolve-pr-review-thread",
681692
];
682693

683694
/// Validate that write-requiring safe-outputs have a write service connection configured.
@@ -759,6 +770,118 @@ pub fn validate_update_work_item_target(front_matter: &FrontMatter) -> Result<()
759770
Ok(())
760771
}
761772

773+
/// Validate that submit-pr-review has a required `allowed-events` field when configured.
774+
///
775+
/// An empty or missing `allowed-events` list would allow agents to cast any review vote,
776+
/// including auto-approvals. Operators must explicitly opt in to each allowed event.
777+
pub fn validate_submit_pr_review_events(front_matter: &FrontMatter) -> Result<()> {
778+
if let Some(config_value) = front_matter.safe_outputs.get("submit-pr-review") {
779+
if let Some(obj) = config_value.as_object() {
780+
let allowed_events = obj.get("allowed-events");
781+
let is_empty = match allowed_events {
782+
None => true,
783+
Some(v) => v.as_array().map_or(true, |a| a.is_empty()),
784+
};
785+
if is_empty {
786+
anyhow::bail!(
787+
"safe-outputs.submit-pr-review requires a non-empty 'allowed-events' list \
788+
to prevent agents from casting unrestricted review votes. Example:\n\n \
789+
safe-outputs:\n submit-pr-review:\n allowed-events:\n \
790+
- comment\n - approve-with-suggestions\n\n\
791+
Valid events: approve, approve-with-suggestions, request-changes, comment\n"
792+
);
793+
}
794+
} else {
795+
anyhow::bail!(
796+
"safe-outputs.submit-pr-review must be a configuration object with an \
797+
'allowed-events' list. Example:\n\n \
798+
safe-outputs:\n submit-pr-review:\n allowed-events:\n - comment\n"
799+
);
800+
}
801+
}
802+
Ok(())
803+
}
804+
805+
/// Validate that update-pr has a required `allowed-votes` field when the `vote` operation
806+
/// is enabled (i.e., `allowed-operations` is empty — meaning all ops — or explicitly contains
807+
/// "vote").
808+
///
809+
/// An empty `allowed-votes` list when vote is enabled would always fail at Stage 2 with a
810+
/// runtime error. Catching this at compile time is consistent with how
811+
/// `validate_submit_pr_review_events` handles the analogous case.
812+
pub fn validate_update_pr_votes(front_matter: &FrontMatter) -> Result<()> {
813+
if let Some(config_value) = front_matter.safe_outputs.get("update-pr") {
814+
if let Some(obj) = config_value.as_object() {
815+
// Determine whether the vote operation is reachable:
816+
// - allowed-operations absent or empty → all operations allowed (includes vote)
817+
// - allowed-operations non-empty → vote is allowed only if explicitly listed
818+
let vote_reachable = match obj.get("allowed-operations") {
819+
None => true,
820+
Some(v) => v
821+
.as_array()
822+
.map_or(true, |a| a.is_empty() || a.iter().any(|x| x == "vote")),
823+
};
824+
825+
if vote_reachable {
826+
let allowed_votes_empty = match obj.get("allowed-votes") {
827+
None => true,
828+
Some(v) => v.as_array().map_or(true, |a| a.is_empty()),
829+
};
830+
if allowed_votes_empty {
831+
anyhow::bail!(
832+
"safe-outputs.update-pr enables the 'vote' operation but has no \
833+
'allowed-votes' list. This would reject all votes at Stage 2. \
834+
Either restrict 'allowed-operations' to exclude 'vote', or add an \
835+
explicit 'allowed-votes' list:\n\n \
836+
safe-outputs:\n update-pr:\n allowed-votes:\n \
837+
- approve-with-suggestions\n - wait-for-author\n\n\
838+
Valid votes: approve, approve-with-suggestions, reject, \
839+
wait-for-author, reset\n"
840+
);
841+
}
842+
}
843+
}
844+
// If the value is a scalar (e.g. `update-pr: true`) we don't error here —
845+
// the config will default to empty allowed-votes, which is safe (vote always rejected).
846+
}
847+
Ok(())
848+
}
849+
850+
/// Validate that resolve-pr-review-thread has a required `allowed-statuses` field when configured.
851+
///
852+
/// An empty or missing `allowed-statuses` list would let agents set any thread status,
853+
/// including "fixed" or "wontFix" on security-critical review threads. Operators must
854+
/// explicitly opt in to each allowed status transition.
855+
pub fn validate_resolve_pr_thread_statuses(front_matter: &FrontMatter) -> Result<()> {
856+
if let Some(config_value) = front_matter.safe_outputs.get("resolve-pr-review-thread") {
857+
if let Some(obj) = config_value.as_object() {
858+
let allowed_statuses = obj.get("allowed-statuses");
859+
let is_empty = match allowed_statuses {
860+
None => true,
861+
Some(v) => v.as_array().map_or(true, |a| a.is_empty()),
862+
};
863+
if is_empty {
864+
anyhow::bail!(
865+
"safe-outputs.resolve-pr-review-thread requires a non-empty \
866+
'allowed-statuses' list to prevent agents from manipulating thread \
867+
statuses without explicit operator consent. Example:\n\n \
868+
safe-outputs:\n resolve-pr-review-thread:\n allowed-statuses:\n\
869+
\x20 - fixed\n\n\
870+
Valid statuses: active, fixed, wont-fix, closed, by-design\n"
871+
);
872+
}
873+
} else {
874+
anyhow::bail!(
875+
"safe-outputs.resolve-pr-review-thread must be a configuration object \
876+
with an 'allowed-statuses' list. Example:\n\n \
877+
safe-outputs:\n resolve-pr-review-thread:\n allowed-statuses:\n\
878+
\x20 - fixed\n"
879+
);
880+
}
881+
}
882+
Ok(())
883+
}
884+
762885
#[cfg(test)]
763886
mod tests {
764887
use super::*;
@@ -1350,4 +1473,167 @@ mod tests {
13501473
let result = generate_pipeline_path(&abs_path);
13511474
assert_eq!(result, "{{ workspace }}/agents/ctf.yml");
13521475
}
1476+
1477+
// ─── validate_submit_pr_review_events ────────────────────────────────────
1478+
1479+
#[test]
1480+
fn test_submit_pr_review_events_passes_when_not_configured() {
1481+
let fm = minimal_front_matter();
1482+
assert!(validate_submit_pr_review_events(&fm).is_ok());
1483+
}
1484+
1485+
#[test]
1486+
fn test_submit_pr_review_events_fails_when_allowed_events_missing() {
1487+
let (fm, _) = parse_markdown(
1488+
"---\nname: test\ndescription: test\nsafe-outputs:\n submit-pr-review:\n allowed-repositories:\n - self\n---\n"
1489+
).unwrap();
1490+
let result = validate_submit_pr_review_events(&fm);
1491+
assert!(result.is_err());
1492+
let msg = result.unwrap_err().to_string();
1493+
assert!(msg.contains("allowed-events"), "message: {msg}");
1494+
}
1495+
1496+
#[test]
1497+
fn test_submit_pr_review_events_fails_when_allowed_events_empty() {
1498+
let (fm, _) = parse_markdown(
1499+
"---\nname: test\ndescription: test\nsafe-outputs:\n submit-pr-review:\n allowed-events: []\n---\n"
1500+
).unwrap();
1501+
let result = validate_submit_pr_review_events(&fm);
1502+
assert!(result.is_err());
1503+
let msg = result.unwrap_err().to_string();
1504+
assert!(msg.contains("allowed-events"), "message: {msg}");
1505+
}
1506+
1507+
#[test]
1508+
fn test_submit_pr_review_events_fails_when_value_is_scalar() {
1509+
let (fm, _) = parse_markdown(
1510+
"---\nname: test\ndescription: test\nsafe-outputs:\n submit-pr-review: true\n---\n"
1511+
).unwrap();
1512+
let result = validate_submit_pr_review_events(&fm);
1513+
assert!(result.is_err());
1514+
}
1515+
1516+
#[test]
1517+
fn test_submit_pr_review_events_passes_when_events_provided() {
1518+
let (fm, _) = parse_markdown(
1519+
"---\nname: test\ndescription: test\nsafe-outputs:\n submit-pr-review:\n allowed-events:\n - comment\n - approve\n---\n"
1520+
).unwrap();
1521+
assert!(validate_submit_pr_review_events(&fm).is_ok());
1522+
}
1523+
1524+
// ─── validate_update_pr_votes ─────────────────────────────────────────────
1525+
1526+
#[test]
1527+
fn test_update_pr_votes_passes_when_not_configured() {
1528+
let fm = minimal_front_matter();
1529+
assert!(validate_update_pr_votes(&fm).is_ok());
1530+
}
1531+
1532+
#[test]
1533+
fn test_update_pr_votes_fails_when_vote_reachable_and_no_allowed_votes() {
1534+
// allowed-operations absent → vote is reachable; no allowed-votes → should fail
1535+
let (fm, _) = parse_markdown(
1536+
"---\nname: test\ndescription: test\nsafe-outputs:\n update-pr:\n allowed-repositories:\n - self\n---\n"
1537+
).unwrap();
1538+
let result = validate_update_pr_votes(&fm);
1539+
assert!(result.is_err());
1540+
let msg = result.unwrap_err().to_string();
1541+
assert!(msg.contains("allowed-votes"), "message: {msg}");
1542+
}
1543+
1544+
#[test]
1545+
fn test_update_pr_votes_fails_when_vote_explicit_and_no_allowed_votes() {
1546+
// allowed-operations contains "vote"; no allowed-votes → should fail
1547+
let (fm, _) = parse_markdown(
1548+
"---\nname: test\ndescription: test\nsafe-outputs:\n update-pr:\n allowed-operations:\n - vote\n---\n"
1549+
).unwrap();
1550+
let result = validate_update_pr_votes(&fm);
1551+
assert!(result.is_err());
1552+
let msg = result.unwrap_err().to_string();
1553+
assert!(msg.contains("allowed-votes"), "message: {msg}");
1554+
}
1555+
1556+
#[test]
1557+
fn test_update_pr_votes_fails_when_allowed_votes_empty() {
1558+
// allowed-operations absent; allowed-votes is empty list → should fail
1559+
let (fm, _) = parse_markdown(
1560+
"---\nname: test\ndescription: test\nsafe-outputs:\n update-pr:\n allowed-votes: []\n---\n"
1561+
).unwrap();
1562+
let result = validate_update_pr_votes(&fm);
1563+
assert!(result.is_err());
1564+
}
1565+
1566+
#[test]
1567+
fn test_update_pr_votes_passes_when_vote_excluded_from_allowed_operations() {
1568+
// allowed-operations is non-empty and does not contain "vote" → safe, no error
1569+
let (fm, _) = parse_markdown(
1570+
"---\nname: test\ndescription: test\nsafe-outputs:\n update-pr:\n allowed-operations:\n - add-reviewers\n - set-auto-complete\n---\n"
1571+
).unwrap();
1572+
assert!(validate_update_pr_votes(&fm).is_ok());
1573+
}
1574+
1575+
#[test]
1576+
fn test_update_pr_votes_passes_when_vote_reachable_and_allowed_votes_set() {
1577+
// allowed-operations absent; allowed-votes non-empty → OK
1578+
let (fm, _) = parse_markdown(
1579+
"---\nname: test\ndescription: test\nsafe-outputs:\n update-pr:\n allowed-votes:\n - approve-with-suggestions\n---\n"
1580+
).unwrap();
1581+
assert!(validate_update_pr_votes(&fm).is_ok());
1582+
}
1583+
1584+
#[test]
1585+
fn test_update_pr_votes_passes_when_vote_explicit_and_allowed_votes_set() {
1586+
// allowed-operations contains "vote"; allowed-votes non-empty → OK
1587+
let (fm, _) = parse_markdown(
1588+
"---\nname: test\ndescription: test\nsafe-outputs:\n update-pr:\n allowed-operations:\n - vote\n allowed-votes:\n - wait-for-author\n---\n"
1589+
).unwrap();
1590+
assert!(validate_update_pr_votes(&fm).is_ok());
1591+
}
1592+
1593+
// ─── validate_resolve_pr_thread_statuses ──────────────────────────────────
1594+
1595+
#[test]
1596+
fn test_resolve_pr_thread_passes_when_not_configured() {
1597+
let fm = minimal_front_matter();
1598+
assert!(validate_resolve_pr_thread_statuses(&fm).is_ok());
1599+
}
1600+
1601+
#[test]
1602+
fn test_resolve_pr_thread_fails_when_allowed_statuses_missing() {
1603+
let (fm, _) = parse_markdown(
1604+
"---\nname: test\ndescription: test\nsafe-outputs:\n resolve-pr-review-thread:\n allowed-repositories:\n - self\n---\n"
1605+
).unwrap();
1606+
let result = validate_resolve_pr_thread_statuses(&fm);
1607+
assert!(result.is_err());
1608+
let msg = result.unwrap_err().to_string();
1609+
assert!(msg.contains("allowed-statuses"), "message: {msg}");
1610+
}
1611+
1612+
#[test]
1613+
fn test_resolve_pr_thread_fails_when_allowed_statuses_empty() {
1614+
let (fm, _) = parse_markdown(
1615+
"---\nname: test\ndescription: test\nsafe-outputs:\n resolve-pr-review-thread:\n allowed-statuses: []\n---\n"
1616+
).unwrap();
1617+
let result = validate_resolve_pr_thread_statuses(&fm);
1618+
assert!(result.is_err());
1619+
let msg = result.unwrap_err().to_string();
1620+
assert!(msg.contains("allowed-statuses"), "message: {msg}");
1621+
}
1622+
1623+
#[test]
1624+
fn test_resolve_pr_thread_fails_when_value_is_scalar() {
1625+
let (fm, _) = parse_markdown(
1626+
"---\nname: test\ndescription: test\nsafe-outputs:\n resolve-pr-review-thread: true\n---\n"
1627+
).unwrap();
1628+
let result = validate_resolve_pr_thread_statuses(&fm);
1629+
assert!(result.is_err());
1630+
}
1631+
1632+
#[test]
1633+
fn test_resolve_pr_thread_passes_when_statuses_provided() {
1634+
let (fm, _) = parse_markdown(
1635+
"---\nname: test\ndescription: test\nsafe-outputs:\n resolve-pr-review-thread:\n allowed-statuses:\n - fixed\n - wont-fix\n---\n"
1636+
).unwrap();
1637+
assert!(validate_resolve_pr_thread_statuses(&fm).is_ok());
1638+
}
13531639
}

src/compile/onees.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ use super::common::{
2424
generate_pipeline_resources, generate_pr_trigger, generate_repositories,
2525
generate_schedule, generate_source_path, generate_working_directory,
2626
replace_with_indent, validate_comment_target, validate_update_work_item_target,
27-
validate_write_permissions,
27+
validate_write_permissions, validate_submit_pr_review_events,
28+
validate_update_pr_votes, validate_resolve_pr_thread_statuses,
2829
};
2930
use super::types::{FrontMatter, McpConfig};
3031

@@ -139,6 +140,12 @@ displayName: "Finalize""#,
139140
validate_comment_target(front_matter)?;
140141
// Validate update-work-item has required target field
141142
validate_update_work_item_target(front_matter)?;
143+
// Validate submit-pr-review has required allowed-events field
144+
validate_submit_pr_review_events(front_matter)?;
145+
// Validate update-pr vote operation has required allowed-votes field
146+
validate_update_pr_votes(front_matter)?;
147+
// Validate resolve-pr-review-thread has required allowed-statuses field
148+
validate_resolve_pr_thread_statuses(front_matter)?;
142149

143150
// Replace all template markers
144151
let compiler_version = env!("CARGO_PKG_VERSION");

src/compile/standalone.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ use super::common::{
2121
generate_pipeline_path, generate_pipeline_resources, generate_pr_trigger,
2222
generate_repositories, generate_schedule, generate_source_path,
2323
generate_working_directory, replace_with_indent, sanitize_filename,
24-
validate_write_permissions, validate_comment_target, validate_update_work_item_target,
24+
validate_write_permissions, validate_comment_target, validate_update_work_item_target, validate_submit_pr_review_events,
25+
validate_update_pr_votes, validate_resolve_pr_thread_statuses,
2526
};
2627
use super::types::{FrontMatter, McpConfig};
2728
use crate::allowed_hosts::{CORE_ALLOWED_HOSTS, mcp_required_hosts};
@@ -130,6 +131,12 @@ impl Compiler for StandaloneCompiler {
130131
validate_comment_target(front_matter)?;
131132
// Validate update-work-item has required target field
132133
validate_update_work_item_target(front_matter)?;
134+
// Validate submit-pr-review has required allowed-events field
135+
validate_submit_pr_review_events(front_matter)?;
136+
// Validate update-pr vote operation has required allowed-votes field
137+
validate_update_pr_votes(front_matter)?;
138+
// Validate resolve-pr-review-thread has required allowed-statuses field
139+
validate_resolve_pr_thread_statuses(front_matter)?;
133140

134141
// Load threat analysis prompt template
135142
let threat_analysis_prompt = include_str!("../../templates/threat-analysis.md");

0 commit comments

Comments
 (0)