Skip to content

Add dismiss-review safe output with actor-bound PR review dismissal guards#43125

Open
pelikhan with Copilot wants to merge 6 commits into
mainfrom
copilot/add-dismiss-review-safe-output
Open

Add dismiss-review safe output with actor-bound PR review dismissal guards#43125
pelikhan with Copilot wants to merge 6 commits into
mainfrom
copilot/add-dismiss-review-safe-output

Conversation

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

This introduces a new safe output for dismissing pull request reviews from the workflow actor (defaulting to github-actions[bot] from action context). It enforces three invariants: justification length (>= 20), caller-supplied author must equal the inferred actor, and the fetched review author must also equal that actor before dismissal.

  • Safe-output surface area (config + schemas)

    • Added dismiss-pull-request-review with alias dismiss-review to safe-output config parsing and handler descriptors.
    • Added schema support in workflow/frontmatter and agent-output unions.
    • Added tool definitions for dismiss_pull_request_review in both runtime and workflow-side tool catalogs.
  • MCP-side validation

    • Added pre-validation handler for dismiss_pull_request_review to reject short justification and actor/author mismatches before recording output.
    • Wired tool loader mapping so the new handler is selected in the safe-output MCP path.
  • Runtime execution guardrails

    • Added dismiss_pull_request_review.cjs handler to:
      • resolve target PR/repo using existing safe-output target semantics,
      • infer actor from current action context,
      • fetch review and verify author identity,
      • call pulls.dismissReview only after identity + justification checks pass.
  • Registry/types wiring

    • Registered the new handler in safe-output manager/registry paths and threat-policy classification.
    • Added JS safe-output type declaration for dismiss_pull_request_review.
safe-outputs:
  dismiss-review:
    max: 10
    target: triggering
{
  "type": "dismiss_pull_request_review",
  "review_id": 12345,
  "justification": "This review is stale after significant follow-up changes.",
  "author": "github-actions[bot]"
}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Add dismiss-review safe output for PR review dismissal Add dismiss-review safe output with actor-bound PR review dismissal guards Jul 3, 2026
Copilot AI requested a review from pelikhan July 3, 2026 06:22
@pelikhan pelikhan marked this pull request as ready for review July 3, 2026 06:36
Copilot AI review requested due to automatic review settings July 3, 2026 06:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new safe output (dismiss-pull-request-review / alias dismiss-review) that allows workflows to dismiss an existing PR review, with guardrails intended to ensure the dismissal is actor-bound and justified.

Changes:

  • Added dismiss_pull_request_review to the agent-output schema, workflow/frontmatter schema, and safe-output validation config (including min justification length).
  • Wired the new safe output through workflow compilation/tool catalogs and safe-output registry/enablement logic.
  • Implemented MCP-side pre-validation plus a runtime JS handler (with tests) that fetches the review and dismisses it only after identity checks.
Show a summary per file
File Description
schemas/agent-output.json Adds DismissPullRequestReviewOutput to the agent output union schema.
pkg/workflow/unified_prompt_step.go Enables dismiss_pull_request_review tool exposure when configured.
pkg/workflow/tool_description_enhancer.go Adds config-derived constraints for the new tool description.
pkg/workflow/safe_outputs_validation.go Includes new safe-output key in target validation pass.
pkg/workflow/safe_outputs_validation_config.go Adds per-field validation rules for dismiss_pull_request_review.
pkg/workflow/safe_outputs_tools_repo_params.go Enables repo parameter behavior (allowed-repos/target-repo) for the tool.
pkg/workflow/safe_outputs_tools_computation.go Includes tool in enabled-tools computation.
pkg/workflow/safe_outputs_state.go Counts new config as “safe outputs enabled” for state/guards.
pkg/workflow/safe_outputs_max_validation.go Validates max for the new config type.
pkg/workflow/safe_outputs_handler_registry.go Registers handler config emission for runtime processing.
pkg/workflow/safe_outputs_config.go Adds DismissPullRequestReview config + frontmatter parsing (incl alias).
pkg/workflow/safe_output_handlers.go Registers safe-output descriptor + permissions for dismiss-review.
pkg/workflow/js/safe_outputs_tools.json Adds the tool definition to the workflow-side JS tool catalog.
pkg/workflow/dismiss_pull_request_review.go New Go config parser/type for dismissing PR reviews.
pkg/parser/schemas/main_workflow_schema.json Adds workflow schema support for dismiss-pull-request-review and alias.
actions/setup/js/types/safe-outputs.d.ts Adds TS item type for dismiss_pull_request_review.
actions/setup/js/safe_outputs_tools.json Adds the tool definition to the action runtime tool catalog.
actions/setup/js/safe_outputs_tools_loader.cjs Wires tool name to the new MCP pre-validation handler.
actions/setup/js/safe_outputs_handlers.test.cjs Adds tests for MCP-side justification + author/actor checks.
actions/setup/js/safe_outputs_handlers.cjs Adds MCP-side pre-validation handler for justification/author checks.
actions/setup/js/safe_output_handler_manager.cjs Registers runtime handler path + threat classification.
actions/setup/js/dismiss_pull_request_review.test.cjs New runtime handler tests for author/actor and fetched-author guards.
actions/setup/js/dismiss_pull_request_review.cjs New runtime handler that resolves target/repo, validates author, fetches review, and dismisses it.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 22/23 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment on lines +514 to +515
"name": "dismiss_pull_request_review",
"description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (typically github-actions[bot]). You must provide a justification of at least 20 characters.",
Comment thread pkg/workflow/js/safe_outputs_tools.json Outdated
Comment on lines +658 to +659
"name": "dismiss_pull_request_review",
"description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (typically github-actions[bot]). You must provide a justification of at least 20 characters.",
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (263 new lines in pkg/ and actions/ directories, threshold: 100) but does not have a linked Architecture Decision Record (ADR).

Draft ADR committed: docs/adr/43125-add-actor-bound-dismiss-review-safe-output.md — review and complete it before merging.

This PR cannot merge until an ADR is linked in the PR body.

What to do next
  1. Review the draft ADR committed to your branch at docs/adr/43125-add-actor-bound-dismiss-review-safe-output.md — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer (e.g., the motivating workflow use case that required this), refine the decision rationale, and confirm or replace the listed alternatives
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-43125: Add Actor-Bound Dismiss-Review Safe Output

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 43125-add-actor-bound-dismiss-review-safe-output.md for PR #43125).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 47.8 AIC · ⌖ 10.1 AIC · ⊞ 4.2K ·
Comment /review to run again

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100 — Excellent

Analyzed 6 test(s): 6 design, 0 implementation, 0 violation(s).

📊 Metrics (6 tests)
Metric Value
Analyzed 6 (Go: 0, JS: 6)
✅ Design 6 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 4 (67%)
Duplicate clusters 0
Inflation No
🚨 Violations 0
Test File Classification Issues
dismisses a review when author matches current actor dismiss_pull_request_review.test.cjs design_test · behavioral_contract happy-path only
rejects when provided author differs from current actor dismiss_pull_request_review.test.cjs design_test · behavioral_contract · high_value none
rejects when fetched review author differs from current actor dismiss_pull_request_review.test.cjs design_test · behavioral_contract · high_value none
should write entry and return success with valid justification safe_outputs_handlers.test.cjs design_test · behavioral_contract happy-path only
should throw MCP error when justification is shorter than 20 characters safe_outputs_handlers.test.cjs design_test · behavioral_contract · high_value none
should throw MCP error when author does not match current workflow actor safe_outputs_handlers.test.cjs design_test · behavioral_contract · high_value none

Verdict

Passed. 0% implementation tests (threshold: 30%). All 6 tests enforce the actor-bound security invariant — 4 of 6 cover error/guard paths directly. No violations, no test inflation (ratios: 0.70:1 and 1.92:1 respectively).

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 30.3 AIC · ⌖ 14.6 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 90/100. 0% implementation tests (threshold: 30%).

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Summary

Applied /tdd and /codebase-design. The PR is well-structured and the two-phase validation approach is solid. Nine inline comments were posted covering the most impactful issues:

# File Issue
1 dismiss_pull_request_review.cjs:85 Misleading supportsPR=false comment
2 dismiss_pull_request_review.cjs:107 Staged mode bypasses review-author identity check
3 dismiss_pull_request_review.cjs:27 Actor resolution priority (env before context.actor)
4 dismiss_pull_request_review.test.cjs Missing edge-case tests (boundary, max-count, staged)
5 safe_outputs_handlers.cjs:329 Duplicated actor-resolution logic
6 dismiss_pull_request_review.cjs:139 Dead fallback in error message
7 dismiss_pull_request_review.test.cjs:176 ESM import in .cjs test file
8 safe_outputs_validation_config.go:895 MaxBodyLength is wrong constant for justification
9 dismiss_pull_request_review.cjs:151 author in success response should reflect validated actor

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 96.9 AIC · ⌖ 9.43 AIC · ⊞ 6.6K ·
Comment /matt to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 96.9 AIC · ⌖ 9.43 AIC · ⊞ 6.6K
Comment /matt to run again

Comments that could not be inline-anchored

actions/setup/js/dismiss_pull_request_review.cjs:85

[/codebase-design] The comment supportsPR=false means PR-only handlers inverts the obvious reading — a false value named supportsPR would normally imply the opposite. This will confuse future maintainers unfamiliar with resolveTarget conventions.

<details>
<summary>💡 Suggestion</summary>

Consider renaming to requiresPR: true and updating resolveTarget to accept that semantics, or at minimum change the comment to be explicit: `// false = handler targets PR only (no issue fallb…

actions/setup/js/dismiss_pull_request_review.cjs:107

[/tdd] In staged mode, the identity check against the fetched review author is skipped entirely — the staged path calls processedCount++ and returns success without fetching the review. This means staged mode can produce misleading results: it would succeed even when the review's author isn't the current actor, masking a runtime failure.

<details>
<summary>💡 Suggestion</summary>

Either document explicitly that staged mode skips review-author verification, or move the staged branch belo…

actions/setup/js/dismiss_pull_request_review.cjs:27

[/codebase-design] getEffectiveActor() reads from process.env.GITHUB_ACTOR first, then context?.actor — but in the runtime handler (inside the factory closure), context is the global GitHub Actions context object. However, process.env.GITHUB_ACTOR might differ from context.actor in certain matrix or reusable-workflow scenarios. Since this value is used as the security boundary for dismissal, prefer context?.actor as the authoritative source and keep env as a fallback only.

actions/setup/js/dismiss_pull_request_review.test.cjs:289

[/tdd] The test file only covers three scenarios. Missing edge cases that this security-sensitive handler should cover: (1) max-count enforcement — call the handler max+1 times and assert the last call fails; (2) review_id validation — zero, negative, non-numeric strings; (3) justification boundary — exactly 19 characters (fail) and exactly 20 characters (pass); (4) staged mode — verify correct return shape and that no GitHub API calls are made.

<details>
<summary>💡 Suggested addition…

actions/setup/js/safe_outputs_handlers.cjs:329

[/codebase-design] The MCP handler duplicates the actor-resolution logic from dismiss_pull_request_review.cjs (getEffectiveActor). Both inline process.env.GITHUB_ACTOR || context?.actor || &quot;github-actions[bot]&quot;. If the resolution logic ever changes (e.g., priority reordering), it must be updated in two places.

<details>
<summary>💡 Suggestion</summary>

Extract actor resolution into a shared helper — e.g., a getWorkflowActor() function in safe_output_helpers.cjs — and import it i…

actions/setup/js/dismiss_pull_request_review.cjs:139

[/tdd] The error message uses reviewAuthor || &quot;unknown&quot; but reviewAuthor at this point has already passed the empty-string guard on line 129–134 (it is always a non-empty string here). The || &quot;unknown&quot; fallback is dead code and creates an impression that reviewAuthor might still be empty, which could confuse readers.

<details>
<summary>💡 Fix</summary>

// reviewAuthor is guaranteed non-empty by the guard above — remove the || &#39;unknown&#39; fallback
error: `review author (${revie…

</details>

<details><summary>actions/setup/js/dismiss_pull_request_review.test.cjs:176</summary>

**[/tdd]** The test file uses `import` syntax on line 176 (`import { describe, it, expect, beforeEach, vi } from &quot;vitest&quot;`) but the module it tests is a CJS module (`.cjs`). The `require` call on line 233 will work in CJS interop but mixing ES module imports with CJS `require` in Vitest can lead to subtle module-isolation issues. Other test files in this codebase appear to use a consistent pattern  ensure this matches.

&lt;details&gt;
&lt;summary&gt;💡 Suggestion&lt;/summary&gt;

Check whether peer test files 

</details>

<details><summary>pkg/workflow/safe_outputs_validation_config.go:895</summary>

**[/codebase-design]** The justification `MaxLength` is set to `MaxBodyLength` (same cap as comment bodies). A dismissal justification that approaches a comment-body length would be nonsensical and signals the wrong semantics to callers. This may also produce a misleading error message that references a body length instead of a justification length.

&lt;details&gt;
&lt;summary&gt;💡 Suggestion&lt;/summary&gt;

Use a smaller, purpose-appropriate cap — e.g., 500 or 1000 characters — with a clear constant name lik…

</details>

<details><summary>actions/setup/js/dismiss_pull_request_review.cjs:151</summary>

**[/tdd]** The success response does not increment `processedCount` until after `dismissReview` succeeds, which is correct. However, the success response returns `author: reviewAuthor` (the fetched review author) rather than `author: expectedAuthor` (which was validated against the actor). If they differ due to case normalisation the return value will be misleading. Both are guaranteed equal at this point, so use the more semantically meaningful one, or return both for diagnostic purposes.

&lt;de…

</details>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — 3 correctness/security issues + 1 test gap

Three issues in the new dismiss_pull_request_review handler need fixing before merge.

Blocking issues summary

1. Stale actor snapshot (medium)dismisser is captured once at main() factory time while the MCP pre-validation phase re-reads GITHUB_ACTOR fresh each call. The two layers enforce actor-identity against potentially different values. Fix: call getEffectiveActor() inside the returned handler, not at factory init. (Line 37)

2. parseInt silent truncation on review_id (medium)Number.parseInt("123abc", 10) returns 123, silently dismissing the wrong review. The Go-layer IsPositiveInteger guard is strict and protects production paths, but the runtime handler should defend itself rather than relying on upstream pipeline integrity. Fix: use a round-trip check (String(Number(rawId)) !== rawId) to reject non-pure-integer strings. (Line 50)

3. Staged mode returns unverified author (medium) — staged responses include author: expectedAuthor without verifying it against the GitHub API. This makes staged output structurally misleading for callers who rely on it to preview what would happen in real mode. Fix: omit author from staged responses or annotate it as unverified. (Line 111)

4. Thin test coverage on a security-gating handler (medium) — only 3 test cases exist; max-count budget, invalid review_id, staged mode, and short justification paths all lack coverage. (dismiss_pull_request_review.test.cjs)

🔎 Code quality review by PR Code Quality Reviewer · 216.4 AIC · ⌖ 8.3 AIC · ⊞ 5.4K
Comment /review to run again

const githubClient = await createAuthenticatedGitHubClient(config);
const requiredLabels = Array.isArray(config.required_labels) ? config.required_labels : [];
const requiredTitlePrefix = config.required_title_prefix || "";
const dismisser = getEffectiveActor();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale actor snapshot breaks actor-identity coherence between MCP and runtime phases: dismisser is captured once at main() factory time, but the MCP pre-validation handler re-reads GITHUB_ACTOR fresh on every call. If the environment changes between factory creation and invocation, the two layers enforce identity against different values.

💡 Suggested fix

getEffectiveActor() is a pure, side-effect-free function — there is no reason to cache its result. Moving the call inside the returned handler makes both layers consistent:

// Remove the factory-level capture:
// const dismisser = getEffectiveActor();  // <-- DELETE

return async function handleDismissPullRequestReview(message) {
  const dismisser = getEffectiveActor(); // read per invocation
  // ... rest unchanged ...
};

Note: the test suite sets process.env.GITHUB_ACTOR in beforeEach after the module is required — because main() is called inside beforeEach too, the tests would catch a per-call read correctly, but they do not surface the drift caused by a factory-time snapshot.

}

const reviewId = Number.parseInt(String(message.review_id || ""), 10);
if (!Number.isInteger(reviewId) || reviewId <= 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseInt silently truncates malformed review IDs: Number.parseInt("123abc", 10) returns 123 — the trailing non-numeric characters are silently dropped, so the handler proceeds against review 123 instead of rejecting the input.

💡 Details and suggested fix

The Go-layer IsPositiveInteger check (which uses strconv.ParseInt) runs before runtime execution and would reject "123abc". However the runtime handler should not rely on upstream validation as its only guard — a defensive strict check here prevents silent data corruption if the validation pipeline changes or is bypassed.

// Current (fragile — silently truncates "123abc" -> 123):
const reviewId = Number.parseInt(String(message.review_id || ""), 10);
if (!Number.isInteger(reviewId) || reviewId <= 0) { ... }

// Safer — reject if the full string is not a pure integer:
const rawId = String(message.review_id ?? "").trim();
const reviewId = Number(rawId);
if (!Number.isInteger(reviewId) || reviewId <= 0 || String(reviewId) !== rawId) {
  return { success: false, error: "review_id must be a positive integer" };
}

The same truncation bug is present in safe_output_type_validator.cjs's validatePositiveInteger, but that is pre-existing and outside this diff.

pull_request_number: pullRequestNumber,
repo: `${owner}/${repo}`,
author: expectedAuthor,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staged mode returns an unverified author field: in staged mode the handler skips the getReview API call (correct — staged mode avoids all API writes), but the success response includes author: expectedAuthor which is the caller-supplied or actor-derived value, not the actual review author on GitHub. A caller relying on the staged response to validate author identity before promoting to real mode gets a false positive.

💡 Details

The real mode fetches the review, validates review.user.login === expectedAuthor, then returns author: reviewAuthor (the API-verified value). The staged mode returns author: expectedAuthor (the local assumption) without any API verification.

This inconsistency makes staged output structurally misleading. Consider either:

  1. Omitting the author field from staged responses (consumers can't trust it anyway), or
  2. Adding a comment like // author unverified in staged mode to make the gap explicit.

There are no tests for staged mode, so this behavioral gap has no coverage.

handler = await main({ max: 10 });
});

it("dismisses a review when author matches current actor", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test suite covers only the happy path and two author-mismatch cases — critical paths are untested: the security contract of this handler rests on actor-identity enforcement and the max-count budget, yet neither has any test coverage.

💡 Missing test cases

Minimum additional tests needed for a security-gating handler:

  1. Max count exhaustion — create handler with max: 1, call it twice; second call must return { success: false, error: /Max count/ }.
  2. Invalid review_id — missing, zero, negative, and string "0" should all fail with review_id must be a positive integer.
  3. Staged mode — handler with staged: true must not call getReview or dismissReview, must return success: true, staged: true.
  4. Short justificationjustification: "short" (< 20 chars) must fail before any API call.
  5. Review author unavailablegetReview returns { data: { user: null } }; handler must return the unavailability error.

The first two are correctness/security; the rest are basic robustness coverage expected for any safe-output handler.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Add dismiss-review safe output with actor-bound PR review dismissal guards

Overall the implementation is well-structured and follows established safe-output patterns throughout the codebase. The defense-in-depth design (MCP pre-validation + runtime identity fetch) is correct, and the wiring across Go config, registry, tool catalog, and JS runtime is complete.

3 issues found — all in the runtime JS handler:

# Severity File Description
1 Medium dismiss_pull_request_review.cjs:50 Float review_id inputs (e.g. 123.5) pass validation after parseInt silently truncates them
2 Low dismiss_pull_request_review.cjs:37 Actor is captured once at factory time; move getEffectiveActor() inside the inner function for per-message evaluation
3 Low dismiss_pull_request_review.cjs:58 Runtime handler's own justification < 20 guard (line 58) is not exercised in dismiss_pull_request_review.test.cjs

Minor nit (non-blocking): The inline comment at line 79 misstates the supportsPR: false semantics — logic is correct but the wording is inverted.

What looks good
  • Permission (NewPermissionsContentsReadPRWrite) matches the pulls.dismissReview API scope.
  • THREAT_WARNING_ABORT_TYPES correctly includes dismiss_pull_request_review.
  • The processedCount is incremented only on success paths (staged and live), preventing limit bypass via retries.
  • Go struct embeds BaseSafeOutputConfig, SafeOutputTargetConfig, and SafeOutputFilterConfig correctly — isSafeOutputHandlerEnabledAndUnstaged will find Staged via the standard reflection path.
  • agent-output.json definition correctly omits secrecy/integrity (those belong in the tool input schema, not the agent output schema — consistent with all other output types).
  • The dismiss-review alias is handled in both the frontmatter parser and the JSON schema $ref, which is the correct dual-registration pattern.
  • Test coverage for the MCP-side handler is thorough (valid case, short justification, author mismatch).

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 170.8 AIC · ⌖ 6.73 AIC · ⊞ 4.9K

}

const reviewId = Number.parseInt(String(message.review_id || ""), 10);
if (!Number.isInteger(reviewId) || reviewId <= 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review_id float inputs are silently truncated.

parseInt("123.5", 10) returns 123, which then satisfies Number.isInteger(123). A caller passing a float review_id would silently target a different review. Consider rejecting non-integer string inputs that contain a decimal:

const rawStr = String(message.review_id || "");
if (!/^\d+$/.test(rawStr)) {
  return { success: false, error: "review_id must be a positive integer" };
}
const reviewId = Number.parseInt(rawStr, 10);

@copilot please address this.

const githubClient = await createAuthenticatedGitHubClient(config);
const requiredLabels = Array.isArray(config.required_labels) ? config.required_labels : [];
const requiredTitlePrefix = config.required_title_prefix || "";
const dismisser = getEffectiveActor();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actor is captured once at factory construction time, not per-call.

dismisser is resolved when main() is called (once per handler lifecycle), not when each message is handled. In most deployments this is fine, but if GITHUB_ACTOR changes after the factory is constructed (e.g., in tests that mutate the environment between calls without re-instantiating the handler), the guard silently uses a stale actor value.

The test currently works around this by setting process.env.GITHUB_ACTOR before calling main() in beforeEach, but calling vi.resetModules() doesn't help if the same handler instance is reused. Consider documenting this contract explicitly, or moving the getEffectiveActor() call inside the inner handler function so it's evaluated per-message:

return async function handleDismissPullRequestReview(message) {
  const dismisser = getEffectiveActor(); // evaluate per call
  ...
};

@copilot please address this.

}

const justification = typeof message.justification === "string" ? message.justification.trim() : "";
if (justification.length < 20) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runtime handler lacks tests for its own justification < 20 guard.

The MCP handler test (safe_outputs_handlers.test.cjs) validates the MCP-layer rejection of short justifications, but dismiss_pull_request_review.test.cjs (the runtime handler) has no test exercising line 58's rejection path. Since the runtime handler runs in the GitHub Actions step (not the MCP phase), this code path is untested end-to-end.

Add a test case like:

it("rejects when justification is shorter than 20 characters", async () => {
  const result = await handler({
    type: "dismiss_pull_request_review",
    review_id: 123,
    justification: "too short",
  });
  expect(result.success).toBe(false);
  expect(result.error).toContain("at least 20 characters");
  expect(mockDismissReview).not.toHaveBeenCalled();
});

@copilot please address this.

context,
itemType: "pull request review dismissal",
// In resolveTarget conventions, supportsPR=false means PR-only handlers.
supportsPR: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading inline comment on supportsPR: false.

The comment reads: "In resolveTarget conventions, supportsPR=false means PR-only handlers." This is backwards: supportsPR=false does not mean PR-only by positive assertion. It means the handler does not support issues. PR-only is the implied default when both supportsPR and supportsIssue are false. The JSDoc for resolveTarget says: "When false, handler supports PRs ONLY." The logic is correct; the comment is just confusing and could lead to future misreads.

Suggested replacement:

// supportsPR: false + supportsIssue: false (defaults) → PR-only handler per resolveTarget contract\nsupportsPR: false,\n```

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.
Open review feedback to address:

  • copilot-pull-request-reviewer:

Run: https://github.com/github/gh-aw/actions/runs/28645166299

Generated by 👨‍🍳 PR Sous Chef · 4.8 AIC · ⌖ 4.46 AIC · ⊞ 3.1K ·
Comment /souschef to run again

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category feature
Risk 🔴 High
Score 72 / 100
Action fast_track

Score Breakdown: Impact 40 + Urgency 18 + Quality 14

Rationale: Adds new dismiss-review safe output with actor-bound PR review dismissal guards (761+, 24 files). High-value new safe-output capability. Currently has CHANGES_REQUESTED from 2 automated reviews flagging 3 correctness/security issues + 1 test gap. Needs author to address review feedback before merge.

⚠️ Blocked by: CHANGES_REQUESTED — 3 correctness/security issues identified in dismiss_pull_request_review.cjs

Recommended Action: Author must address review issues; then fast-track for human approval.

Generated by 🔧 PR Triage Agent · 86.3 AIC · ⌖ 6.42 AIC · ⊞ 5.5K ·

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.
Open review feedback to address:

  • copilot-pull-request-reviewer: ...

Addressed the unresolved review feedback in commit 6958b01 by correcting the dismiss_pull_request_review description to reference github.actor / GITHUB_ACTOR (instead of saying it is typically github-actions[bot]) in both tool catalogs. Local validation passed (make fmt, make lint, make test-unit, make test, make recompile), and parallel validation also passed.

CI on this new head is stale after agent push and needs a maintainer re-trigger.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants