Skip to content

Prevent invalid id-token: read emission when merging permissions: all: read with explicit overrides#43297

Merged
pelikhan merged 3 commits into
mainfrom
copilot/fix-id-token-read-scope
Jul 4, 2026
Merged

Prevent invalid id-token: read emission when merging permissions: all: read with explicit overrides#43297
pelikhan merged 3 commits into
mainfrom
copilot/fix-id-token-read-scope

Conversation

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

permissions: all: read was safe on its own, but when merged with explicit scopes (e.g. copilot-requests: write) the compiler could materialize an explicit permissions map containing id-token: read, which GitHub Actions rejects. This change aligns the all: read expansion path in Set() with existing merge/render guards so invalid id-token levels are never emitted.

  • What changed

    • Updated Permissions.Set (pkg/workflow/permissions_operations.go) to skip PermissionIdToken when converting hasAll + allLevel=read into explicit scope entries.
    • This closes the remaining conversion path that could synthesize id-token: read during map materialization.
  • Regression coverage

    • Extended TestPermissionsSet (pkg/workflow/permissions_operations_test.go) with an all: read + copilot-requests: write case.
    • Added assertions that:
      • PermissionIdToken is not present after conversion.
      • rendered YAML does not contain id-token: read.
p := NewPermissionsAllRead()
p.Set(PermissionCopilotRequests, PermissionWrite)

if _, exists := p.Get(PermissionIdToken); exists {
	t.Fatal("id-token must not be expanded at read level")
}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix invalid id-token: read scope in generated lock file Prevent invalid id-token: read emission when merging permissions: all: read with explicit overrides Jul 4, 2026
Copilot AI requested a review from pelikhan July 4, 2026 04:31
@pelikhan pelikhan marked this pull request as ready for review July 4, 2026 06:33
Copilot AI review requested due to automatic review settings July 4, 2026 06:33
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

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

No ADR enforcement needed: PR #43297 does not have the 'implementation' label and has only 13 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

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 pull request fixes a GitHub Actions compatibility bug in gh-aw’s permissions compiler: when permissions: all: read is merged/materialized into an explicit permissions map (via Permissions.Set()), it could previously synthesize id-token: read, which GitHub Actions rejects. The change aligns Set()’s “all: read” expansion behavior with existing guards in Get()/Merge()/RenderToYAML() so invalid id-token levels are never emitted.

Changes:

  • Updated Permissions.Set() to skip expanding id-token when converting all: read into an explicit permissions map.
  • Added a regression test to ensure id-token is not present after conversion and rendered YAML never contains id-token: read.
Show a summary per file
File Description
pkg/workflow/permissions_operations.go Prevents Set() from materializing an invalid id-token: read entry when expanding all: read.
pkg/workflow/permissions_operations_test.go Adds a regression test covering all: read plus explicit overrides (e.g., copilot-requests: write) and asserts the rendered YAML is valid.

Review details

Tip

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

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

@github-actions github-actions Bot mentioned this pull request Jul 4, 2026

@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: Prevent invalid id-token: read emission in Set()

The fix is correct, minimal, and consistent with the existing pattern across the codebase. All other expansion paths (Merge, RenderToYAML, Get, shorthand expansion) already guard against id-token: read; the Set() hasAll branch was the only missing site.

Correctness:

  • The guard s == PermissionIdToken && p.allLevel == PermissionRead exactly mirrors lines 164, 281, 296, 338, and 380 in the same file — this is the correct pattern.
  • p.hasAll = false / p.allLevel = "" are cleared after expansion (lines 187-188), so Get() and RenderToYAML() correctly see the map-only state post-Set().
  • The all: write path is unaffected; id-token: write is valid.

Test coverage: The new test case directly exercises the regression: NewPermissionsAllRead() + Set(PermissionCopilotRequests, PermissionWrite), then asserts id-token absent from map and YAML. Both assertions are necessary and sufficient.

No issues found.

Warning

Firewall blocked 1 domain

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

  • proxy.golang.org

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

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 37 AIC · ⌖ 5.73 AIC · ⊞ 4.9K

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100 — Excellent

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

📊 Metrics (1 test scenario)
Metric Value
Analyzed 1 (Go: 1, JS: 0)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 1 (100%)
Duplicate clusters 0
Inflation Minor (9 test lines / 4 prod lines = 2.25:1)
🚨 Violations 0
Test File Classification Issues
TestPermissionsSet — p3 block permissions_operations_test.go:92 ✅ design_test / behavioral_contract Minor inflation (2.25:1)

Analysis

The new test block added to TestPermissionsSet exercises the precise behavioral contract described in the PR title: when NewPermissionsAllRead() is converted to explicit permissions via Set(), id-token must not appear in the resulting map or YAML output.

Two assertions validate complementary perspectives:

  1. Internal state: p3.Get(PermissionIdToken) confirms id-token is absent from the expanded permissions map.
  2. User-visible output: p3.RenderToYAML() confirms id-token: read is absent from the rendered YAML (the actual artifact consumed downstream).

Both carry descriptive failure messages. Build tag //go:build !integration is present on line 1. No forbidden mocks. No duplicates.

The minor inflation flag (2.25:1) is marginal and appropriate given the two-assertion coverage style for a targeted bug fix.

Verdict

Passed. 0% implementation tests (threshold: 30%). No guideline violations.

References:

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 · 29.7 AIC · ⌖ 10.5 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%). No guideline violations.

@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 /diagnosing-bugs — commenting with minor improvement suggestions. The fix and test coverage are solid; no blocking issues.

📋 Key Themes & Highlights

Key Themes

  • Test assertion precision: The YAML assertion checks for id-token: read specifically but should check for any id-token: entry to fully capture the intent.
  • Test structure consistency: The existing permission tests use a well-structured table-driven pattern; the new inline case could be aligned with that style.
  • Comment precision: The implementation comment could be made more concrete by citing the GitHub Actions constraint explicitly.

Positive Highlights

  • ✅ Root cause correctly identified and fixed: the all: read expansion path in Set() was the last gap not covered by existing guards.
  • ✅ Fix is minimal and surgical — 4 lines, exactly mirroring the already-correct guard in the shorthand expansion path above it.
  • ✅ Regression test is present and directly reproduces the failure scenario (all: read + copilot-requests: write).
  • ✅ Both the map state and rendered YAML are asserted, giving full coverage of the bug's two observable symptoms.

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

if _, exists := p3.Get(PermissionIdToken); exists {
t.Error("expected id-token to be excluded when converting all: read to explicit map")
}
if yaml := p3.RenderToYAML(); strings.Contains(yaml, "id-token: read") {

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.

[/tdd] The YAML assertion only guards against id-token: read, but the intent is that id-token should be entirely absent — any other accidental level (e.g. none) would slip through.

💡 Stronger assertion

Change the assertion to check for any id-token: entry, not just the read level:

if yaml := p3.RenderToYAML(); strings.Contains(yaml, "id-token:") {
    t.Errorf("RenderToYAML() should not contain any id-token entry, got:\n%s", yaml)
}

This matches the intent of the fix — id-token should be completely absent from the output when starting from all: read.

@copilot please address this.

t.Errorf("expected issues: write, got %v (exists: %v)", level, exists)
}

p3 := NewPermissionsAllRead()

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.

[/tdd] The new test case is embedded flat inside TestPermissionsSet, while the existing test for the shorthand conversion case (TestPermissionsSetPreservesShorthandPermissions) uses a structured table-driven pattern. Embedding p3 inline makes it harder to see what scenario is being tested.

💡 Suggested refactor

Either move this case into TestPermissionsSetPreservesShorthandPermissions as a new table row, or at minimum add a descriptive comment block:

// all: read + explicit write scope must not expand id-token to any level
p3 := NewPermissionsAllRead()
p3.Set(PermissionCopilotRequests, PermissionWrite)

Even better, consider adding a parallel entry in TestPermissionsSetPreservesShorthandPermissions that tests the all: read case with an assertion for PermissionIdToken absent, keeping all permission-preservation tests in one place.

@copilot please address this.

for _, s := range GetAllPermissionScopes() {
if _, exists := p.permissions[s]; !exists {
// id-token does not support the read level
if s == PermissionIdToken && p.allLevel == PermissionRead {

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.

[/diagnosing-bugs] The guard is correct and well-placed. One minor note: the comment says id-token does not support the read level — it would be more precise to say it supports only write and none (not any non-write level), which matches how GitHub Actions actually validates permissions.

💡 Suggested comment wording
// id-token only supports "write" and "none"; emitting "read" causes GitHub Actions to reject
// the workflow. Skip it entirely when expanding from all: read.

This mirrors the comment in permissions_rendering.go (if one exists) and makes the rule concrete for future readers — clarifying that this isn't a gh-aw policy but a GitHub Actions constraint.

@copilot please address this.

@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.

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

if s == PermissionIdToken && p.allLevel == PermissionRead {
continue
}
p.permissions[s] = p.allLevel

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.

PermissionDiscussions has the same gap this PR just closed for PermissionIdToken: Set() expands all: read into an explicit map but does not skip discussions, causing discussions: read to materialize in the explicit map where the RenderToYAML guard no longer fires.

💡 Explanation and suggested fix

When Set() is called on an all: read Permissions, it expands all scopes into p.permissions. This PR correctly skips id-token during that expansion. However, RenderToYAML also suppresses discussions: read (GHE compatibility) — but only when rendering through the hasAll branch. Once Set() has materialized discussions: read into the explicit map, the RenderToYAML guard on the hasAll branch never runs, and the scope leaks into the final YAML.

Add the matching guard alongside the id-token one in the hasAll block:

// id-token does not support the read level
if s == PermissionIdToken && p.allLevel == PermissionRead {
    continue
}
// discussions: read is suppressed for GHE compatibility (only include if explicitly set)
if s == PermissionDiscussions && p.allLevel == PermissionRead {
    continue
}

The test added by this PR should also assert that discussions is absent from the converted map.

}
if yaml := p3.RenderToYAML(); strings.Contains(yaml, "id-token: read") {
t.Errorf("RenderToYAML() should not contain id-token: read, got:\n%s", yaml)
}

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 does not assert the boundary case: all: write still correctly expands id-token: write: The test only verifies that id-token is absent after all: read conversion, but does not verify the complementary case — that the guard is not applied when allLevel == PermissionWrite, which could silently regress.

💡 Suggested additional assertion

Add a case verifying the guard is not over-applied:

// Guard must NOT fire at write level — id-token: write is valid
p4 := &Permissions{hasAll: true, allLevel: PermissionWrite, permissions: make(map[PermissionScope]PermissionLevel)}
p4.Set(PermissionCopilotRequests, PermissionWrite)
if level, exists := p4.Get(PermissionIdToken); !exists || level != PermissionWrite {
    t.Errorf("expected id-token: write after all: write expansion, got level=%q exists=%v", level, exists)
}

Without this, a future change that over-broadly guards PermissionIdToken for all levels would pass the current test suite.

@pelikhan

pelikhan commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

@copilot merge main, recompile, lint go

…read-scope

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

@copilot merge main, recompile, lint go

Done in 8b91607: merged main, ran make recompile, and ran Go lint (make golint).

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category bug
Risk 🟢 Low
Score 72 / 100
Action fast_track
Batch pr-batch:compiler-fixes

Score breakdown: Impact 38 (permissions correctness — prevents GHA runtime rejections) + Urgency 22 + Quality 12 (small fix, github-actions approved)

Fixes compiler emitting id-token: read when permissions: all: read is merged with explicit scopes. Minimal +13 line change. Ready for human sign-off.

Generated by 🔧 PR Triage Agent · 82.8 AIC · ⌖ 8.09 AIC · ⊞ 5.5K ·

@pelikhan pelikhan merged commit 993aecf into main Jul 4, 2026
29 checks passed
@pelikhan pelikhan deleted the copilot/fix-id-token-read-scope branch July 4, 2026 07:44
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.

ll: <level> + write-only scope expands to invalid id-token: read in generated lock file

3 participants