Skip to content

Fix recurring gh-aw-firewall digest-pin loss on DefaultFirewallVersion bumps - #51423

Merged
pelikhan merged 5 commits into
mainfrom
copilot/gh-aw-firewall-fix-regression
Aug 8, 2026
Merged

Fix recurring gh-aw-firewall digest-pin loss on DefaultFirewallVersion bumps#51423
pelikhan merged 5 commits into
mainfrom
copilot/gh-aw-firewall-fix-regression

Conversation

Copilot AI commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

gh-aw-firewall/{agent,api-proxy,squid,cli-proxy} images were silently emitted as bare tags instead of digest-pinned references (image:tag@sha256:...) in compiled lock files whenever constants.DefaultFirewallVersion was bumped — a fourth recurrence of the same class of bug (previously "fixed" in #38561, #43307, #44040).

Root cause

ActionCache.PruneStaleContainerPins removes any container pin no longer referenced by locally compiled lock files. When gh-aw's own default AWF version is bumped, the previous version's pin becomes unreferenced in gh-aw's own repo and gets pruned from .github/aw/actions-lock.json. make sync-action-pins then mirrors that loss into the embedded pkg/actionpins/data/action_pins.json / pkg/workflow/data/action_pins.json catalogs shipped in the binary, breaking digest pinning for any consumer workflow explicitly pinning that now-previous version — with no trace in resolution_failures.

Changes

  • Exempt firewall images from pruning (pkg/workflow/action_cache.go): PruneStaleContainerPins now skips any image under constants.DefaultFirewallRegistry, so resolved AWF digest pins are retained across version bumps instead of being dropped.
  • Surface unresolved pins (pkg/workflow/docker.go): applyContainerPins records a resolution_failures manifest entry when a gh-aw-firewall image has no cache/embedded pin, instead of silently shipping an unpinned tag.
  • Golden/compile test: added TestCompileWorkflow_AllManifestContainersArePinned, which parses the compiled lock file's manifest and asserts every containers[] entry carries a pinned_image — the regression-catching test suggested in the issue.
  • Regression test: TestCompileWorkflow_UnresolvedFirewallImageRecordsResolutionFailure verifies an unresolvable AWF version now surfaces a resolution failure rather than compiling silently.
  • Updated/added unit tests for the new pruning-exemption behavior (pkg/workflow/action_cache_container_pin_test.go, pkg/cli/update_container_pins_test.go).
// Before: firewall pins pruned like any other stale entry
delete(c.ContainerPins, image)

// After: gh-aw-firewall images are never pruned once resolved
if strings.HasPrefix(image, constants.DefaultFirewallRegistry+"/") {
    continue // retained across version bumps
}

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 13.5 AIC · ⌖ 6.44 AIC · ⊞ 8.5K ·
Comment /souschef to run again

…ng, record resolution failures

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

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for tackling the digest-pinning regression in #51248! 👋 This is an important security fix—preventing silent sandbox changes via mutable tag repush is critical.

Here are a few things to complete:

  • Implement the fix — The PR is currently empty (0 changes). Add the actual code changes to fix the regression in the compiler where three firewall images (agent, api-proxy, squid) are losing their digest pins in v0.86.1.
  • Add a golden/compile test — As noted in the issue, this is the fourth recurrence of this bug. A test asserting that every entry in containers[] carries a pinned_image would catch similar regressions at compile time.
  • Update PR description — Once implementation is underway, update the PR body to reflect the specific files changed and approach taken.

When you're ready to continue, here's a prompt for your agent:

Fix the digest-pinning regression in gh-aw v0.86.1 for firewall images (agent, api-proxy, squid).

1. Identify why the compiler stopped emitting `pinned_image` for these three images between v0.86.0 and v0.86.1.
2. Implement the fix in the compiler's image resolution logic.
3. Add a golden/compile test that verifies every image in the compiled `containers[]` array has a `pinned_image` field.
4. Verify the fix by compiling the test workflow from #51248 with both v0.86.0 behavior expectations and the fix.

Generated by ✅ Contribution Check · auto · 66 AIC · ⌖ 3.95 AIC · ⊞ 8.7K ·

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix regression of firewall digest-pinning in v0.86.1 Fix recurring gh-aw-firewall digest-pin loss on DefaultFirewallVersion bumps Aug 8, 2026
Copilot AI requested a review from pelikhan August 8, 2026 17:07
@pelikhan
pelikhan marked this pull request as ready for review August 8, 2026 17:10
Copilot AI balanced review requested due to automatic review settings August 8, 2026 17:10

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

Prevents firewall digest pins from being pruned and surfaces unresolved pins in compiled manifests.

Changes:

  • Retains historical firewall container pins.
  • Records unresolved firewall pin failures.
  • Adds compile and pruning regression tests.
Show a summary per file
File Description
pkg/workflow/docker.go Records missing firewall pins.
pkg/workflow/docker_firewall_pin_compile_test.go Tests manifest pinning and failures.
pkg/workflow/action_cache.go Exempts firewall pins from pruning.
pkg/workflow/action_cache_container_pin_test.go Tests pruning behavior.
pkg/cli/update_container_pins_test.go Tests CLI cache retention.

Review details

Tip

Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/workflow/docker.go
// security-load-bearing containers (they confine the agent sandbox), so
// record the miss as a resolution failure for lock-file auditing instead
// of silently shipping an unpinned tag (see gh-aw#51248).
if workflowData != nil && strings.HasPrefix(img, constants.DefaultFirewallRegistry+"/") {
if setutil.Contains(knownImages, image) {
continue
}
if strings.HasPrefix(image, constants.DefaultFirewallRegistry+"/") {
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

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

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Warning

Firewall blocked 1 domain

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

  • api.individual.githubcopilot.com

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

network:
  allowed:
    - defaults
    - "api.individual.githubcopilot.com"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

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

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

🧪 Test quality analysis by Test Quality Sentinel

@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: Fix recurring gh-aw-firewall digest-pin loss on DefaultFirewallVersion bumps

The fix is well-structured and addresses the root cause: PruneStaleContainerPins now exempts all ghcr.io/github/gh-aw-firewall/ images from pruning, and applyContainerPins records a resolution_failure when an AWF image tag has no cached digest pin.

Existing review comments already flagged the two most important concerns:

  1. docker.go line 258 — The resolution-failure path is reached only for truly unresolved images (the continue on line 249 guards the already-pinned path), so the existing comment is more cautionary than blocking.

  2. action_cache.go line 180 — The exemption is forward-only; pins already dropped before this fix are not automatically restored. Worth noting in release notes or a follow-up.

The tests are thorough: TestUpdateContainerPins_KeepsStaleFirewallEntries, TestPruneStaleContainerPins, and TestCompileWorkflow_UnresolvedFirewallImageRecordsResolutionFailure together give solid coverage. No blocking issues found beyond the already-commented concerns.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 26.4 AIC · ⌖ 7.21 AIC · ⊞ 5.5K

@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 /diagnosing-bugs and /tdd — requesting changes on two correctness issues before merge.

📋 Key Themes & Highlights

Key Themes

  • Root cause addressed correctly: exempting DefaultFirewallRegistry images from PruneStaleContainerPins is the right fix and well-documented.
  • Detection layer is good: surfacing unresolved pins as resolution_failures instead of silently shipping bare tags closes the observability gap described in #51248.
  • Test coverage is thorough: golden compile test + regression test + unit tests for the pruning exemption form a solid regression suite.

Correctness Issues

  1. docker.go line 258 — The HasPrefix guard triggers even when img already contains @sha256:, creating a spurious resolution_failure for an already-pinned reference. Add !strings.Contains(img, "@sha256:") to the condition.
  2. docker_firewall_pin_compile_test.go line 319imageTag strips the leading v but there is no confirmation that the compiler-side tag construction does the same. If the Ref field actually contains the v prefix, the test will always fail with a misleading message. Verify consistency and add the actual failures to the error message for diagnosability.
  3. Operational gap (noted in review comment, not blocking code) — The exemption prevents future pin loss but does not restore already-lost historical pins. A follow-up make sync-action-pins run is needed post-merge.

Positive Highlights

  • ✅ Excellent commit comment explaining why the exemption is safe (cheap, few KB per version).
  • TestUpdateContainerPins_KeepsStaleFirewallEntries directly tests the regression scenario from #51248.
  • TestCompileWorkflow_AllManifestContainersArePinned is the generic golden test the issue called for — will catch future additions that forget pinning.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 37.7 AIC · ⌖ 7.8 AIC · ⊞ 7.1K
Comment /matt to run again

Comment thread pkg/workflow/docker.go
// security-load-bearing containers (they confine the agent sandbox), so
// record the miss as a resolution failure for lock-file auditing instead
// of silently shipping an unpinned tag (see gh-aw#51248).
if workflowData != nil && strings.HasPrefix(img, constants.DefaultFirewallRegistry+"/") {

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 HasPrefix check fires for an already-digest-pinned reference (e.g. ghcr.io/github/gh-aw-firewall/agent:0.27.0@sha256:abc) — this would add a spurious resolution_failure for an image that is already pinned.

💡 Suggested guard

Skip recording the failure when the image string already contains a digest:

if workflowData != nil &&
    strings.HasPrefix(img, constants.DefaultFirewallRegistry+"/") &&
    !strings.Contains(img, "`@sha256`:") {
    // record resolution failure ...
}

@copilot please address this.

if setutil.Contains(knownImages, image) {
continue
}
if strings.HasPrefix(image, constants.DefaultFirewallRegistry+"/") {

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 exemption prevents future pruning but does not restore any pins that were already lost before this PR lands. Any consumer workflow that was broken by a previous DefaultFirewallVersion bump still needs a one-time make sync-action-pins run (or equivalent) to repopulate the embedded catalogs (pkg/actionpins/data/action_pins.json, pkg/workflow/data/action_pins.json).

💡 Suggestion

Add a note in the PR description or a follow-up task to re-resolve missing historical AWF pins so the binary ships with a complete catalog. This is a post-merge operational step, not a code change, but without it the fix only prevents future losses — it does not heal the current state.

@copilot please address this.

}
if len(unpinned) > 0 {
t.Errorf("Expected every manifest container entry to carry digest/pinned_image, but %d were unpinned: %v", len(unpinned), unpinned)
}

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] imageTag is computed with strings.TrimPrefix(unresolvedVersion, "v") but unresolvedVersion is "v0.1.2-unresolved-test" — so imageTag is "0.1.2-unresolved-test". Verify that the AWF image construction path also strips the v prefix the same way; if the compiler keeps the v prefix the assertion f.Ref == imageTag will always fail and the test will report a false-negative ("no resolution failure found") even when the feature works.

💡 Suggestion

Add a sub-assertion or log the actual manifest.ResolutionFailures slice when the test fails so it's immediately clear what Ref value was actually recorded:

if !found {
    t.Errorf("Expected resolution failure for %s:%s, got: %+v",
        agentRepo, imageTag, manifest.ResolutionFailures)
}

Also confirm the compiler-side tag normalisation matches before merging.

@copilot please address this.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 82/100 — Excellent

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

📊 Metrics (21 tests)
Metric Value
Analyzed 21 (Go: 21, JS: 0)
✅ Design 20 (95%)
⚠️ Implementation 1 (5%)
Edge/error coverage 17 (81%)
Duplicate clusters 0
Inflation Yes (docker_firewall_pin_compile_test.go +134 vs docker.go +19 = 7:1 — acceptable)
🚨 Violations 0
Test File Classification Issues
TestCollectImagesFromLockFiles update_container_pins_test.go design_test / behavioral_contract
TestCollectImagesFromLockFiles_MissingDir update_container_pins_test.go design_test / high_value
TestBuildxDigestPattern update_container_pins_test.go design_test / high_value
TestUpdateContainerPins_PinnedLockFilesPreserveContainerPins update_container_pins_test.go design_test / behavioral_contract
TestUpdateContainerPins_PrunesStaleEntries update_container_pins_test.go design_test / high_value
TestUpdateContainerPins_KeepsStaleFirewallEntries update_container_pins_test.go design_test / behavioral_contract
TestUpdateContainerPins_RefreshExistingPin update_container_pins_test.go design_test / high_value
TestUpdateContainerPins_FailOnResolveErrorsOption update_container_pins_test.go design_test / high_value
TestContainerPinCRUD action_cache_container_pin_test.go design_test / behavioral_contract
TestContainerPinSaveLoad action_cache_container_pin_test.go design_test / high_value
TestContainerPinBackwardCompatibility action_cache_container_pin_test.go design_test / high_value
TestContainerPinMarshalSortedOutput action_cache_container_pin_test.go implementation_test / low_value Sorted-output ordering is an internal implementation detail
TestPruneStaleContainerPins action_cache_container_pin_test.go design_test / behavioral_contract
TestPruneStaleContainerPins_AllStale action_cache_container_pin_test.go design_test / high_value
TestPruneStaleContainerPins_NoneStale action_cache_container_pin_test.go design_test / high_value
TestPruneStaleContainerPins_NilMap action_cache_container_pin_test.go design_test / high_value
TestCompileWorkflow_FirewallImagesPinnedForAWF0270 docker_firewall_pin_compile_test.go design_test / behavioral_contract
TestCompileWorkflow_FirewallImagesPinnedForDefaultVersion docker_firewall_pin_compile_test.go design_test / behavioral_contract
TestCompileWorkflow_BuildToolsImagePinnedForArcDind docker_firewall_pin_compile_test.go design_test / behavioral_contract
TestCompileWorkflow_AllManifestContainersArePinned docker_firewall_pin_compile_test.go design_test / behavioral_contract
TestCompileWorkflow_UnresolvedFirewallImageRecordsResolutionFailure docker_firewall_pin_compile_test.go design_test / behavioral_contract
i️ Notes

Inflation note: docker_firewall_pin_compile_test.go adds 134 lines against docker.go's 19 added production lines (7:1). This is acceptable: the test exercises the full compilation pipeline end-to-end and each new test directly prevents a distinct regression class. Inflation points are withheld per scoring rubric, hence score of 82 instead of 90.

Standout tests:

  • TestCompileWorkflow_AllManifestContainersArePinned — the golden test that parses the real compiled manifest and asserts every container entry carries a non-empty digest/pinned_image. This will catch any future container family that silently loses its pin without needing to enumerate images by name.
  • TestCompileWorkflow_UnresolvedFirewallImageRecordsResolutionFailure — verifies that unresolvable AWF versions surface resolution_failures instead of compiling silently, closing the auditability gap from the issue.
  • TestUpdateContainerPins_KeepsStaleFirewallEntries — unit test for the firewall-exemption pruning rule, directly testing the invariant introduced in PruneStaleContainerPins.

Minor improvement opportunity: Several t.Errorf calls in docker_firewall_pin_compile_test.go do not print the actual yamlStr content on failure, which would slow debugging. Consider appending \nactual lock file:\n%s to the format string. Not a violation.

Verdict

Passed. 5% implementation tests (threshold: 30%). 0 violations. Score: 82/100.

References: Run #31268764740

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 57.6 AIC · ⌖ 7.35 AIC · ⊞ 7.7K ·
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: 82/100. 5% implementation tests (threshold: 30%). 0 violations.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/51423-exempt-firewall-images-from-container-pin-pruning.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 — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  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-51423: Exempt gh-aw-firewall Images from Container Pin Pruning

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., 51423-exempt-firewall-images-from-container-pin-pruning.md for PR #51423).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 50.8 AIC · ⌖ 14 AIC · ⊞ 8.8K ·
Comment /review to run again

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot Quick triage nudge for this PR.

Please address the current blocking review feedback, link the committed ADR in the PR body, refresh the branch if needed, run the pr-finisher skill, and push follow-up fixes.

Open review context (newest first):

  • github-actions — changes requested: spurious firewall resolution_failure for already-pinned @sha256 images in docker.go; verify/fix the firewall image-tag test assumption and improve failure diagnostics.
  • github-actions — ADR gate: add the committed ADR link to the PR body so the gate can clear.

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

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 13.5 AIC · ⌖ 6.44 AIC · ⊞ 8.5K ·
Comment /souschef to run again

Copilot AI requested a review from gh-aw-bot August 8, 2026 17:55
@pelikhan
pelikhan merged commit d0bcb09 into main Aug 8, 2026
30 checks passed
@pelikhan
pelikhan deleted the copilot/gh-aw-firewall-fix-regression branch August 8, 2026 18:20
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PR Triage

  • Category: bug
  • Risk: high
  • Score: 78/100 (Impact 38 + Urgency 22 + Quality 18)
  • Recommendation: fast_track

Fourth recurrence of firewall digest-pin loss on version bumps — exempts gh-aw-firewall images from stale-pin pruning and adds golden/regression tests (TestCompileWorkflow_AllManifestContainersArePinned, unresolved-pin resolution-failure test). Touches action cache/docker pinning (critical supply-chain path), warranting high risk despite full CI pass. AI reviewer initially requested changes, later approved.

Generated by 🔧 PR Triage Agent · auto · 32.2 AIC · ⌖ 2.68 AIC · ⊞ 8K ·

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.86.2

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.

gh-aw-firewall/{agent,api-proxy,squid} emitted tag-only in v0.86.1 lock files (regression of #43307 / #38561)

4 participants