Skip to content

Emit sink-visibility in compiled write-sink guard policies#44154

Merged
lpcox merged 4 commits into
mainfrom
copilot/compiler-emit-sink-visibility
Jul 8, 2026
Merged

Emit sink-visibility in compiled write-sink guard policies#44154
lpcox merged 4 commits into
mainfrom
copilot/compiler-emit-sink-visibility

Conversation

Copilot AI commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

The compiler derived write-sink guard policies for safe-outputs and other non-GitHub MCP sinks, but it did not include the target repository’s visibility. That left public-repo workflows relying on runtime override logic instead of compile-time policy, weakening the primary defense against private-to-public safe-output exfiltration.

  • Compile-time repo visibility

    • Resolve the workflow repository visibility during compilation from GET /repos/{owner}/{repo}.
    • Normalize visibility and fall back to the private boolean when needed.
    • Carry the resolved value through WorkflowData so it is available where MCP config is rendered.
  • Write-sink policy emission

    • Extend derived write-sink policies to include sink-visibility when repository visibility is known.
    • Apply this both to:
      • explicit GitHub guard-policy derivation, and
      • auto-lockdown-derived write-sink policies.
    • Preserve existing accept derivation behavior.
  • Coverage

    • Add focused tests for:
      • public, private, and internal visibility handling,
      • fallback parsing from private,
      • malformed visibility payloads, and
      • rendered MCP configs including sink-visibility.

Example of the emitted config shape:

{
  "guardPolicies": {
    "write-sink": {
      "accept": ["*"],
      "sink-visibility": "public"
    }
  }
}

Copilot AI and others added 2 commits July 8, 2026 02:27
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI changed the title [WIP] Add sink-visibility in write-sink guard policy Emit sink-visibility in compiled write-sink guard policies Jul 8, 2026
Copilot finished work on behalf of lpcox July 8, 2026 02:32
Copilot AI requested a review from lpcox July 8, 2026 02:32
@pelikhan

pelikhan commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@lpcox repo visibility should be resolved at runtime again, since viz changes

@lpcox

lpcox commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@lpcox repo visibility should be resolved at runtime again, since viz changes

yep also checked at runtime (with log warning of policy override)

@lpcox lpcox marked this pull request as ready for review July 8, 2026 02:38
Copilot AI review requested due to automatic review settings July 8, 2026 02:38

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 strengthens compile-time guard policy generation for non-GitHub write sinks by resolving the target repository’s visibility during compilation and emitting it as sink-visibility in derived write-sink guard policies, reducing reliance on runtime override logic.

Changes:

  • Resolve repository visibility at compile time via gh api /repos/{owner}/{repo} with parsing/fallback to the legacy private boolean.
  • Plumb the resolved visibility through WorkflowData and emit sink-visibility in derived write-sink policies (explicit GitHub derivation + auto-lockdown).
  • Add/extend unit tests covering visibility parsing and sink-visibility propagation/rendering.
Show a summary per file
File Description
pkg/workflow/workflow_data.go Adds RepositoryVisibility to WorkflowData so visibility can be carried into render/derivation layers.
pkg/workflow/workflow_builder.go Computes and stores repository visibility during initial workflow data construction (when GitHub tool is present).
pkg/workflow/push_to_pull_request_branch_validation.go Implements visibility fetching/parsing and reuses it for computeIsPublicRepo.
pkg/workflow/mcp_github_config.go Emits sink-visibility in derived write-sink policies when known.
pkg/workflow/safeoutputs_guard_policy_test.go Updates safeoutputs guard-policy derivation tests to assert sink-visibility propagation.
pkg/workflow/non_github_mcp_guard_policy_test.go Extends non-GitHub MCP write-sink policy tests to assert sink-visibility and renderer output.
pkg/workflow/repository_visibility_test.go Adds focused unit tests for visibility parsing, including fallback and malformed payload cases.
pkg/workflow/compiler_orchestrator_workflow_test.go Adds a test asserting repository visibility is set during buildInitialWorkflowData.

Review details

Tip

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

  • Files reviewed: 8/8 changed files
  • Comments generated: 4
  • Review effort level: Low

Comment on lines 21 to 25
// computeIsPublicRepo queries the GitHub API to determine whether the repository
// associated with this compiler instance is publicly visible.
//
// Returns true only when the API confirms visibility == "public". Returns false
// for private/internal repos, when no repository slug is set, or when the API
Comment on lines +57 to +63
func getRepositoryVisibilityForSlug(slug string) (string, error) {
output, err := RunGH("Checking repository visibility...", "api", "/repos/"+slug)
if err != nil {
return "", err
}
return parseRepositoryVisibility(output), nil
}
Comment on lines +14 to +19
var fetchRepositoryVisibility = getRepositoryVisibilityForSlug

type repositoryVisibilityResponse struct {
Visibility string `json:"visibility"`
Private *bool `json:"private"`
}
Comment on lines +121 to +131
func TestBuildInitialWorkflowData_SetsRepositoryVisibilityForGitHubTool(t *testing.T) {
compiler := NewCompiler()
compiler.SetRepositorySlug("github/gh-aw")

originalFetchRepositoryVisibility := fetchRepositoryVisibility
fetchRepositoryVisibility = func(slug string) (string, error) {
assert.Equal(t, "github/gh-aw", slug)
return "public", nil
}
defer func() { fetchRepositoryVisibility = originalFetchRepositoryVisibility }()

@lpcox

lpcox commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@copilot address review feedback

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ failed to deliver outputs during design decision gate check.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@lpcox

lpcox commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Warning

The push_to_pull_request_branch operation failed: Cannot push to pull request branch: bundle modifies files outside the allowed-files list (.github/workflows/ab-testing-advisor.lock.yml, .github/workflows/agent-performance-analyzer.lock.yml, .github/workflows/agent-persona-explorer.lock.yml, .github/workflows/agentic-token-optimizer.lock.yml, .github/workflows/ai-moderator.lock.yml, .github/workflows/approach-validator.lock.yml, .github/workflows/archie.lock.yml, .github/workflows/artifacts-summary.lock.yml, .github/workflows/auto-triage-issues.lock.yml, .github/workflows/avenger.lock.yml, .github/workflows/aw-failure-investigator.lock.yml, .github/workflows/bot-detection.lock.yml, .github/workflows/breaking-change-checker.lock.yml, .github/workflows/chaos-pr-bundle-fuzzer.lock.yml, .github/workflows/ci-coach.lock.yml, .github/workflows/ci-doctor.lock.yml, .github/workflows/claude-code-user-docs-review.lock.yml, .github/workflows/code-scanning-fixer.lock.yml, .github/workflows/code-simplifier.lock.yml, .github/workflows/codex-github-remote-mcp-test.lock.yml, .github/workflows/commit-changes-analyzer.lock.yml, .github/workflows/contribution-check.lock.yml, .github/workflows/copilot-agent-analysis.lock.yml, .github/workflows/copilot-cli-deep-research.lock.yml, .github/workflows/copilot-opt.lock.yml, .github/workflows/copilot-pr-nlp-analysis.lock.yml, .github/workflows/copilot-pr-prompt-analysis.lock.yml, .github/workflows/copilot-session-insights.lock.yml, .github/workflows/craft.lock.yml, .github/workflows/daily-agent-of-the-day-blog-writer.lock.yml, .github/workflows/daily-ambient-context-optimizer.lock.yml, .github/workflows/daily-assign-issue-to-user.lock.yml, .github/workflows/daily-aw-cross-repo-compile-check.lock.yml, .github/workflows/daily-awf-spec-compiler-surfacing.lock.yml, .github/workflows/daily-cache-strategy-analyzer.lock.yml, .github/workflows/daily-caveman-optimizer.lock.yml, .github/workflows/daily-choice-test.lock.yml, .github/workflows/daily-cli-performance.lock.yml, .github/workflows/daily-code-metrics.lock.yml, .github/workflows/daily-community-attribution.lock.yml, .github/workflows/daily-compiler-quality.lock.yml, .github/workflows/daily-compiler-threat-spec-optimizer.lock.yml, .github/workflows/daily-doc-healer.lock.yml, .github/workflows/daily-doc-updater.lock.yml, .github/workflows/daily-experiment-report.lock.yml, .github/workflows/daily-fact.lock.yml, .github/workflows/daily-file-diet.lock.yml, .github/workflows/daily-firewall-report.lock.yml, .github/workflows/daily-formal-spec-verifier.lock.yml, .github/workflows/daily-function-namer.lock.yml, .github/workflows/daily-geo-optimizer.lock.yml, .github/workflows/daily-hippo-learn.lock.yml, .github/workflows/daily-issues-report.lock.yml, .github/workflows/daily-malicious-code-scan.lock.yml, .github/workflows/daily-mcp-concurrency-analysis.lock.yml, .github/workflows/daily-model-inventory.lock.yml, .github/workflows/daily-model-resolution.lock.yml, .github/workflows/daily-multi-device-docs-tester.lock.yml, .github/workflows/daily-news.lock.yml, .github/workflows/daily-observability-report.lock.yml, .github/workflows/daily-performance-summary.lock.yml, .github/workflows/daily-regulatory.lock.yml, .github/workflows/daily-reliability-review.lock.yml, .github/workflows/daily-rendering-scripts-verifier.lock.yml, .github/workflows/daily-repo-chronicle.lock.yml, .github/workflows/daily-safe-output-integrator.lock.yml, .github/workflows/daily-safe-outputs-conformance.lock.yml, .github/workflows/daily-safeoutputs-git-simulator.lock.yml, .github/workflows/daily-secrets-analysis.lock.yml, .github/workflows/daily-security-observability.lock.yml, .github/workflows/daily-security-red-team.lock.yml, .github/workflows/daily-semgrep-scan.lock.yml, .github/workflows/daily-spdd-spec-planner.lock.yml, .github/workflows/daily-team-evolution-insights.lock.yml, .github/workflows/daily-team-status.lock.yml, .github/workflows/daily-testify-uber-super-expert.lock.yml, .github/workflows/daily-windows-terminal-integration-builder.lock.yml, .github/workflows/daily-workflow-updater.lock.yml, .github/workflows/daily-yamllint-fixer.lock.yml, .github/workflows/dataflow-pr-discussion-dataset.lock.yml, .github/workflows/dead-code-remover.lock.yml, .github/workflows/deep-report.lock.yml, .github/workflows/delight.lock.yml, .github/workflows/dependabot-burner.lock.yml, .github/workflows/dependabot-go-checker.lock.yml, .github/workflows/dependabot-repair.lock.yml, .github/workflows/deployment-incident-monitor.lock.yml, .github/workflows/design-decision-gate.lock.yml, .github/workflows/designer-drift-audit.lock.yml, .github/workflows/dev-hawk.lock.yml, .github/workflows/dev.lock.yml, .github/workflows/developer-docs-consolidator.lock.yml, .github/workflows/dictation-prompt.lock.yml, .github/workflows/discussion-task-miner.lock.yml, .github/workflows/draft-pr-cleanup.lock.yml, .github/workflows/eslint-miner.lock.yml, .github/workflows/eslint-monster.lock.yml, .github/workflows/eslint-refiner.lock.yml, .github/workflows/example-permissions-warning.lock.yml, .github/workflows/example-workflow-analyzer.lock.yml, .github/workflows/firewall-escape.lock.yml, .github/workflows/functional-pragmatist.lock.yml, .github/workflows/github-mcp-structural-analysis.lock.yml, .github/workflows/github-mcp-tools-report.lock.yml, .github/workflows/github-remote-mcp-auth-test.lock.yml, .github/workflows/glossary-maintainer.lock.yml, .github/workflows/go-fan.lock.yml, .github/workflows/go-logger.lock.yml, .github/workflows/gpclean.lock.yml, .github/workflows/grumpy-reviewer.lock.yml, .github/workflows/hippo-embed.lock.yml, .github/workflows/hourly-ci-cleaner.lock.yml, .github/workflows/impeccable-skills-reviewer.lock.yml, .github/workflows/instructions-janitor.lock.yml, .github/workflows/issue-arborist.lock.yml, .github/workflows/issue-monster.lock.yml, .github/workflows/issue-triage-agent.lock.yml, .github/workflows/jsweep.lock.yml, .github/workflows/layout-spec-maintainer.lock.yml, .github/workflows/lint-monster.lock.yml, .github/workflows/linter-miner.lock.yml, .github/workflows/mattpocock-skills-reviewer.lock.yml, .github/workflows/mergefest.lock.yml, .github/workflows/metrics-collector.lock.yml, .github/workflows/necromancer.lock.yml, .github/workflows/notion-issue-summary.lock.yml, .github/workflows/objective-impact-report.lock.yml, .github/workflows/org-health-report.lock.yml, .github/workflows/outcome-collector.lock.yml, .github/workflows/plan.lock.yml, .github/workflows/poem-bot.lock.yml, .github/workflows/pr-code-quality-reviewer.lock.yml, .github/workflows/pr-description-caveman.lock.yml, .github/workflows/pr-nitpick-reviewer.lock.yml, .github/workflows/pr-sous-chef.lock.yml, .github/workflows/pr-triage-agent.lock.yml, .github/workflows/prompt-clustering-analysis.lock.yml, .github/workflows/q.lock.yml, .github/workflows/refactoring-cadence.lock.yml, .github/workflows/refiner.lock.yml, .github/workflows/repo-audit-analyzer.lock.yml, .github/workflows/repository-quality-improver.lock.yml, .github/workflows/ruflo-backed-task.lock.yml, .github/workflows/schema-consistency-checker.lock.yml, .github/workflows/schema-feature-coverage.lock.yml, .github/workflows/scout.lock.yml, .github/workflows/security-compliance.lock.yml, .github/workflows/security-review.lock.yml, .github/workflows/semantic-function-refactor.lock.yml, .github/workflows/sergo.lock.yml, .github/workflows/skillet.lock.yml, .github/workflows/smoke-agent-all-merged.lock.yml, .github/workflows/smoke-agent-all-none.lock.yml, .github/workflows/smoke-agent-public-approved.lock.yml, .github/workflows/smoke-agent-public-none.lock.yml, .github/workflows/smoke-agent-scoped-approved.lock.yml, .github/workflows/smoke-antigravity.lock.yml, .github/workflows/smoke-ci.lock.yml, .github/workflows/smoke-claude-on-copilot.lock.yml, .github/workflows/smoke-claude.lock.yml, .github/workflows/smoke-codex.lock.yml, .github/workflows/smoke-copilot-aoai-apikey.lock.yml, .github/workflows/smoke-copilot-aoai-entra.lock.yml, .github/workflows/smoke-copilot-arm.lock.yml, .github/workflows/smoke-copilot.lock.yml, .github/workflows/smoke-create-cross-repo-pr.lock.yml, .github/workflows/smoke-crush.lock.yml, .github/workflows/smoke-gemini.lock.yml, .github/workflows/smoke-opencode.lock.yml, .github/workflows/smoke-otel-backends.lock.yml, .github/workflows/smoke-pi.lock.yml, .github/workflows/smoke-project.lock.yml, .github/workflows/smoke-update-cross-repo-pr.lock.yml, .github/workflows/spec-enforcer.lock.yml, .github/workflows/spec-extractor.lock.yml, .github/workflows/spec-librarian.lock.yml, .github/workflows/stale-pr-cleanup.lock.yml, .github/workflows/stale-repo-identifier.lock.yml, .github/workflows/static-analysis-report.lock.yml, .github/workflows/step-name-alignment.lock.yml, .github/workflows/sub-issue-closer.lock.yml, .github/workflows/technical-doc-writer.lock.yml, .github/workflows/terminal-stylist.lock.yml, .github/workflows/tidy.lock.yml, .github/workflows/typist.lock.yml, .github/workflows/ubuntu-image-analyzer.lock.yml, .github/workflows/uk-ai-operational-resilience.lock.yml, .github/workflows/unbloat-docs.lock.yml, .github/workflows/weekly-blog-post-writer.lock.yml, .github/workflows/weekly-issue-summary.lock.yml, .github/workflows/weekly-safe-outputs-spec-review.lock.yml, .github/workflows/workflow-generator.lock.yml, .github/workflows/workflow-health-manager.lock.yml, .github/workflows/workflow-normalizer.lock.yml). Add the files to the allowed-files configuration field or remove them from the bundle.. The code changes were not applied.

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (208 new lines in pkg/workflow/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/44154-emit-sink-visibility-compiled-write-sink-policies.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/44154-emit-sink-visibility-compiled-write-sink-policies.md — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and verify the alternatives reflect real options considered
  3. Commit any revisions to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-44154: Emit sink-visibility in Compiled write-sink Guard Policies

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

❓ Why ADRs Matter

“AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say ‘I’ll deal with this later.’ Deferring decisions corroded my ability to think clearly.”

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., 44154-emit-sink-visibility-compiled-write-sink-policies.md for PR #44154).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 60.5 AIC · ⌖ 13.6 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 94/100 — Excellent

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

📊 Metrics (5 tests)
Metric Value
Analyzed 5 (Go: 5, JS: 0)
✅ Design 5 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 4 (80%)
Duplicate clusters 0
Inflation No (max ratio 1.1:1)
🚨 Violations 0
Test File Classification Issues
TestBuildInitialWorkflowData_SetsRepositoryVisibilityForGitHubTool compiler_orchestrator_workflow_test.go design_test / behavioral_contract Happy-path only; assertions lack descriptive context
TestDeriveWriteSinkGuardPolicyFromWorkflow (modified) non_github_mcp_guard_policy_test.go design_test / behavioral_contract None
TestAllNonGitHubMCPServersGetWriteSinkWhenGitHubHasAllowOnly (modified) non_github_mcp_guard_policy_test.go design_test / behavioral_contract None
TestParseRepositoryVisibility repository_visibility_test.go design_test / behavioral_contract Assertions lack descriptive failure context
TestDeriveSafeOutputsGuardPolicyFromGitHub (modified) safeoutputs_guard_policy_test.go design_test / behavioral_contract None
⚠️ Flagged Tests (2 — minor style only)

TestBuildInitialWorkflowData_SetsRepositoryVisibilityForGitHubTool (compiler_orchestrator_workflow_test.go) — Happy-path only; no error-path assertion for when fetchRepositoryVisibility returns an error. The assert.Equal calls lack descriptive failure-context strings (Go guideline). Function-variable injection pattern is acceptable. Low severity — the test still enforces the behavioral contract.

TestParseRepositoryVisibility (repository_visibility_test.go) — The assert.Equal in the table loop has no custom failure message. Since t.Run(tt.name, ...) provides identification via subtest names, this is very minor.

Verdict

Passed. 0% implementation tests (threshold: 30%). No build-tag or mock-library violations. New TestParseRepositoryVisibility provides strong edge-case coverage (invalid JSON, invalid field types, both visibility and private fallback paths). The sink-visibility propagation is verified end-to-end across three test suites.

References: §28913461673

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 · 49 AIC · ⌖ 15.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: 94/100. 0% implementation tests (threshold: 30%). All 5 tests enforce behavioral contracts with no violations.

@lpcox lpcox merged commit 65ccc76 into main Jul 8, 2026
29 checks passed
@lpcox lpcox deleted the copilot/compiler-emit-sink-visibility branch July 8, 2026 02:58
Copilot stopped work on behalf of lpcox due to an error July 8, 2026 02:58
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review summary — PR #44154

Requesting changes on 3 findings. The core feature logic is solid; the issues are about redundant I/O, missing edge-case coverage, and seam placement.

📋 Findings (priority order)
# File Issue Skill
1 push_to_pull_request_branch_validation.go:116 computeIsPublicRepo() makes a second gh api call after buildInitialWorkflowData already resolved visibility /tdd
2 non_github_mcp_guard_policy_test.go:442 No test for sinkVisibility: "" — the guard that prevents emitting a blank sink-visibility is untested /tdd
3 push_to_pull_request_branch_validation.go:14 Visibility seam (fetchRepositoryVisibility, parseRepositoryVisibility, etc.) belongs in repository_visibility.go not the push-to-PR file /codebase-design
4 repository_visibility_test.go:50 Missing test for unknown/empty visibility field with no private fallback /tdd
5 compiler_orchestrator_workflow_test.go:151 No test for the negative path: no GitHub tool → RepositoryVisibility stays "" /tdd

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 110.5 AIC · ⌖ 11.3 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.

Skills-Based Review 🧠

Applied /tdd and /codebase-design — requesting changes on two correctness issues (double API call, missing test coverage for the empty-visibility guard path) and one cohesion concern (visibility seam placement).

📋 Key Themes & Highlights

Key Themes

  • Double API call (correctness): validatePushToPullRequestBranchWarnings still calls computeIsPublicRepo()computeRepositoryVisibility()fetchRepositoryVisibility even after buildInitialWorkflowData has already resolved visibility into WorkflowData.RepositoryVisibility. Every compilation of a GitHub-tool workflow makes two gh api /repos/{slug} calls. The fix is to thread the already-resolved string through to the validator.

  • Missing edge-case tests (coverage): The sink-visibility emission guard (if sinkVisibility != "") is the critical safety valve — it prevents emitting a blank field when visibility is unknown. No test exercises the sinkVisibility: "" path, so removing the guard would not be caught by the test suite.

  • Seam placement (design): fetchRepositoryVisibility, repositoryVisibilityResponse, getRepositoryVisibilityForSlug, and parseRepositoryVisibility live in push_to_pull_request_branch_validation.go but are now used across the compilation pipeline. The test file is already named repository_visibility_test.go, signalling a natural home for a repository_visibility.go source file.

Positive Highlights

  • ✅ Robust parseRepositoryVisibility with fallback to the private boolean — handles GHES API responses gracefully
  • ✅ All three visibility values (public, private, internal) covered in tests, including correct internal + private:true pairing
  • sink-visibility added to both explicit-policy and auto-lockdown paths symmetrically
  • RepositoryVisibility gated on GitHub tool presence — no spurious API calls for non-GitHub workflows
  • ✅ Clean refactor: computeIsPublicRepo now delegates to computeRepositoryVisibility, preserving existing call sites

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

Comments that could not be inline-anchored

pkg/workflow/push_to_pull_request_branch_validation.go:116

[/tdd] Double API call: computeIsPublicRepo() re-fetches repository visibility even though buildInitialWorkflowData already resolved it into WorkflowData.RepositoryVisibility. On a workflow with a GitHub tool, compilation makes two gh api /repos/{slug} round-trips.

<details>
<summary>💡 Suggested fix</summary>

Pass the already-resolved visibility through validatePushToPullRequestBranchWarnings so the cached value is reused:

// compiler_validators.go
c.validatePushToPullRe</details>

<details><summary>pkg/workflow/push_to_pull_request_branch_validation.go:14</summary>

**[/codebase-design]** `fetchRepositoryVisibility` is a package-level var now used by both `push_to_pull_request_branch_validation.go` (indirectly via `computeRepositoryVisibility`) and `compiler_orchestrator_workflow_test.go`. Placing this seam in the push-to-PR-branch file makes it easy to miss when other callers need to stub it, and signals that the abstraction belongs at a higher levele.g. on the `Compiler` struct itself or in a dedicated `repository_visibility.go` file.

&lt;details&gt;
&lt;summ</details>

<details><summary>pkg/workflow/repository_visibility_test.go:50</summary>

**[/tdd]** Missing test: when the `visibility` field is present but an unexpected string (e.g. `&quot;enterprise&quot;` or `&quot;&quot;`) and `private` is absent, `parseRepositoryVisibility` returns `&quot;&quot;`. This edge case is not covered, so a future API addition that introduces a new visibility tier would silently emit no `sink-visibility` in policies rather than failing loudly.

&lt;details&gt;
&lt;summary&gt;💡 Suggested test case&lt;/summary&gt;

```go
{
    name:     &quot;unknown visibility with no private field&quot;,
    payload:  `{&quot;v</details>

<details><summary>pkg/workflow/compiler_orchestrator_workflow_test.go:151</summary>

**[/tdd]** The new test only covers the happy path (GitHub tool presentvisibility resolved). There is no test for the guard condition: when the GitHub tool is absent or set to `false`, `RepositoryVisibility` must remain `&quot;&quot;`.

&lt;details&gt;
&lt;summary&gt;💡 Suggested test&lt;/summary&gt;

```go
func TestBuildInitialWorkflowData_NoRepositoryVisibilityWithoutGitHubTool(t *testing.T) {
    compiler := NewCompiler()
    compiler.SetRepositorySlug(&quot;github/gh-aw&quot;)

    // fetchRepositoryVisibility should NOT be …

</details>

<details><summary>pkg/workflow/non_github_mcp_guard_policy_test.go:442</summary>

**[/tdd]** The `TestAllNonGitHubMCPServersGetWriteSinkWhenGitHubHasAllowOnly` assertion on line 442 asserts `sink-visibility` equals `tt.sinkVisibility` unconditionally — but none of the test cases exercise `sinkVisibility: &quot;&quot;`. When visibility is unknown the field must be **absent** from the policy map, not set to an empty string. A test case with empty visibility would catch a regression if the conditional `if sinkVisibility != &quot;&quot;` guard were accidentally removed.

&lt;details&gt;
&lt;summary&gt;💡 Sugge…

</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 — The sink-visibility feature is directionally correct, but three issues need fixing before merge: a double API call that wastes a network round-trip, a mutable package-level function var that will fire the race detector, and a silent parse-failure path that defeats the security guarantee this PR is meant to provide.

### Blocking issues (3)
  1. Double GitHub API call (workflow_builder.go:40): buildInitialWorkflowData fetches visibility and stores it in WorkflowData.RepositoryVisibility, then validatePushToPullRequestBranchWarnings calls computeIsPublicRepo()computeRepositoryVisibility() a second time. The stored value is never used for the boolean check. Fix: derive computeIsPublicRepo from workflowData.RepositoryVisibility.

  2. Race-prone global function var (push_to_pull_request_branch_validation.go:14): fetchRepositoryVisibility is a package-level var swapped by tests. Any concurrent test that hits computeRepositoryVisibility will race. Fix: move the hook onto the Compiler struct.

  3. Silent parse failure (push_to_pull_request_branch_validation.go:62): getRepositoryVisibilityForSlug returns ("", nil) when the API responds with an unrecognizable payload. The caller cannot tell the response was unusable, so sink-visibility is silently omitted — the exact exfiltration-prevention property this PR is hardening.

### Non-blocking issues (4)
  1. Stale doc comment (push_to_pull_request_branch_validation.go:28): The block comment still describes the old bool-returning computeIsPublicRepo semantics.

  2. Test asserts sink-visibility present unconditionally (non_github_mcp_guard_policy_test.go:442): The assertion doesn't account for the empty-visibility branch, and there's no test case for sinkVisibility: "".

  3. Test swaps global without synchronization (compiler_orchestrator_workflow_test.go:126): Same root cause as issue #2.

  4. Subtests missing t.Parallel() (repository_visibility_test.go:52): Outer t.Parallel() is declared but subtests run serially.

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


repositoryVisibility := ""
if rawGitHubTool, hasGitHubTool := toolsResult.tools["github"]; hasGitHubTool && rawGitHubTool != false {
repositoryVisibility = c.computeRepositoryVisibility()

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.

Double GitHub API call per compilation: computeRepositoryVisibility() is called here to populate WorkflowData.RepositoryVisibility, but validatePushToPullRequestBranchWarnings later calls computeIsPublicRepo()computeRepositoryVisibility() again, making a second redundant network round-trip that ignores the already-computed value.

💡 Suggested fix

computeIsPublicRepo should read from the already-populated WorkflowData.RepositoryVisibility instead of re-fetching. Since validatePushToPullRequestBranchWarnings receives the workflow data, the value is already available:

// replace the computeIsPublicRepo() call at the validation call-site with:
isPublic := workflowData.RepositoryVisibility == "public"

Every workflow that uses the GitHub tool and has push-to-pull-request-branch configured currently issues this API call twice, adding latency and burning API rate-limit quota.


var pushToPullRequestBranchValidationLog = newValidationLogger("push_to_pull_request_branch_validation")

var fetchRepositoryVisibility = getRepositoryVisibilityForSlug

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.

Package-level mutable function var is a data-race vector: fetchRepositoryVisibility is a package-level variable that tests swap to inject a fake. Any test in this package that happens to call through computeRepositoryVisibility while running in parallel will race with the swap, which Go's -race detector will flag.

💡 Suggested fix

Move the hook onto the Compiler struct as a field so substitution is scoped to a single instance:

type Compiler struct {
    // ...
    fetchRepositoryVisibility func(slug string) (string, error)
}

func NewCompiler() *Compiler {
    return &Compiler{
        fetchRepositoryVisibility: getRepositoryVisibilityForSlug,
    }
}

Tests then set compiler.fetchRepositoryVisibility = mockFn on their own instance without touching global state. This eliminates the need for the defer-restore pattern in tests entirely.

if err != nil {
return "", err
}
return parseRepositoryVisibility(output), nil

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.

Silent parse failure swallows unrecognizable API responses: getRepositoryVisibilityForSlug returns ("", nil) when parseRepositoryVisibility returns empty — callers cannot distinguish a well-formed but unrecognizable response from a legitimate absence of visibility data, silently leaving RepositoryVisibility empty with no observable signal.

💡 Suggested fix

Return a sentinel error (or at least log a warning) when the API response parsed successfully but no known visibility was found:

func getRepositoryVisibilityForSlug(slug string) (string, error) {
    output, err := RunGH("Checking repository visibility...", "api", "/repos/"+slug)
    if err != nil {
        return "", err
    }
    v := parseRepositoryVisibility(output)
    if v == "" {
        return "", fmt.Errorf("unrecognised visibility in API response for %s", slug)
    }
    return v, nil
}

This surfaces API contract changes or auth-scoping issues at compile time rather than silently emitting policies without sink-visibility, which is exactly the vulnerability this PR exists to prevent.

// call fails (e.g. no authentication, network error). This fail-safe default
// ensures safety warnings are always shown when visibility cannot be determined.
func (c *Compiler) computeIsPublicRepo() bool {
func (c *Compiler) computeRepositoryVisibility() string {

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 doc comment describes the old bool-returning function: The comment block says "Returns true only when..." and "Returns false for private/internal repos" but the function it precedes is now computeRepositoryVisibility returning a string. This will mislead readers into incorrect assumptions about the failure mode.

💡 Suggested fix

Update the comment to match the new signature:

// computeRepositoryVisibility queries the GitHub API to determine the visibility
// of the repository associated with this compiler instance.
//
// Returns "public", "private", or "internal" when the API call succeeds and the
// value is recognized. Returns "" when no repository slug is set, when the slug
// is malformed, when the API call fails, or when the response contains an
// unrecognized visibility value. Callers should treat "" as unknown/private.
func (c *Compiler) computeRepositoryVisibility() string {

compiler.SetRepositorySlug("github/gh-aw")

originalFetchRepositoryVisibility := fetchRepositoryVisibility
fetchRepositoryVisibility = func(slug string) (string, error) {

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 swaps a global without t.Parallel() — race-prone pattern: This test writes to the package-level fetchRepositoryVisibility global without any synchronization. If any other parallel test in this package exercises computeRepositoryVisibility, the race detector will fire.

💡 Suggested fix

The root fix is to move fetchRepositoryVisibility onto the Compiler struct (see companion comment on the declaration). As an immediate workaround, at minimum add t.Parallel() to every test in this file that swaps the global, and add a mutex around the read/write in production code — but that is worse than the struct-field approach.

Until the global is removed, this test should also verify the mutex is held correctly under the Go race detector (go test -race).

writeSink, ok := policies["write-sink"].(map[string]any)
require.True(t, ok, "write-sink should be a map: %s", tt.description)
assert.Equal(t, tt.expectedAccept, writeSink["accept"], "accept list should match: %s", tt.description)
assert.Equal(t, tt.sinkVisibility, writeSink["sink-visibility"], "sink-visibility should match workflow repository visibility: %s", tt.description)

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 unconditionally asserts sink-visibility is always present, but the production code at mcp_github_config.go:68 only emits it when sinkVisibility != "". If sinkVisibility is empty (GitHub tool present, slug unset, or API failure), the policy map will have no sink-visibility key yet this assertion will fail with a confusing message rather than surfacing the real issue.

💡 Suggested fix

The assertion should mirror the production conditional:

if tt.sinkVisibility != "" {
    assert.Equal(t, tt.sinkVisibility, writeSink["sink-visibility"],
        "sink-visibility should match workflow repository visibility: %s", tt.description)
} else {
    assert.NotContains(t, writeSink, "sink-visibility",
        "sink-visibility must be absent when visibility is unknown: %s", tt.description)
}

Also add a test case with sinkVisibility: "" to explicitly verify the omission path, since that is the most security-relevant case (unknown visibility must not emit a stale cached value).

}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

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.

Subtests never call t.Parallel(), negating the benefit of the outer t.Parallel() call on line 12. All subtests run serially within this test.

💡 Suggested fix
for _, tt := range tests {
    tt := tt // capture range variable
    t.Run(tt.name, func(t *testing.T) {
        t.Parallel()
        assert.Equal(t, tt.expected, parseRepositoryVisibility([]byte(tt.payload)))
    })
}

This is the standard Go table-test pattern; without the inner t.Parallel() the subtests are effectively sequential.

@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: Emit sink-visibility in compiled write-sink guard policies

This is a solid, well-scoped security improvement. The logic is correct, test coverage is thorough, and the fallback from private bool is a safe defensive move for GHES/GHEC API variants.

One non-blocking issue found:

There is a redundant API call: workflow_builder.go already resolves repository visibility and caches it in workflowData.RepositoryVisibility. However, validatePushToPullRequestBranchWarnings — called from the validators path with the fully-populated workflowData — still calls computeIsPublicRepo(), which fires a second GET /repos/{slug} request. Since both paths may run in the same compilation, this doubles the network cost for affected workflows. The cached value should be threaded through instead. See inline comment.

Other observations (non-blocking)
  • Stale godoc on computeIsPublicRepo (line 21–27): the block comment still says it's the function that "queries the GitHub API" but that logic now lives in computeRepositoryVisibility. The doc should be updated to reflect that this is a thin bool wrapper.
  • TestBuildInitialWorkflowData_SetsRepositoryVisibilityForGitHubTool test scope: the test constructs the tool map as {"github": map[string]any{}} but the condition in workflow_builder.go is rawGitHubTool != false, which is always true for an empty map. A test case where the GitHub tool is set to false (tool disabled) would confirm the negative case is guarded correctly.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 100.9 AIC · ⌖ 5.93 AIC · ⊞ 4.8K

Comments that could not be inline-anchored

pkg/workflow/push_to_pull_request_branch_validation.go:116

Redundant API call — cached visibility not used here

workflow_builder.go already calls computeRepositoryVisibility() for any workflow with a GitHub tool and stores the result in workflowData.RepositoryVisibility. When that same compilation also triggers this branch (push-to-pull-request-branch with target: &quot;*&quot; and no wildcard fetch), computeIsPublicRepo() fires a second, identical GET /repos/{slug} request.

Since validatePushToPullRequestBranchWarnings is called from `comp…

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.4

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.

Compiler: emit sink-visibility in write-sink guard policy based on target repo visibility

4 participants