From 549e8007f6756acd68fee3a5448a8ef77bed9215 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:47:53 +0000 Subject: [PATCH 1/8] Initial plan From d2232c9d52d80996c54b2b7918c359911ba78e75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:56:22 +0000 Subject: [PATCH 2/8] feat: add digest-aware runtime image tag metadata Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/7e6bdecd-3b0f-43b9-b54e-22e09e83c585 --- .github/workflows/test-action.yml | 7 +-- action.yml | 73 +++++++++++++++++++++++++------ docs/github_actions.md | 2 +- docs/usage.md | 8 ++-- src/cli.ts | 8 +++- src/commands/predownload.test.ts | 27 ++++++++++++ src/commands/predownload.ts | 10 +++-- src/docker-manager.test.ts | 31 +++++++++++++ src/docker-manager.ts | 13 +++--- src/image-tag.ts | 71 ++++++++++++++++++++++++++++++ 10 files changed, 219 insertions(+), 31 deletions(-) create mode 100644 src/image-tag.ts diff --git a/.github/workflows/test-action.yml b/.github/workflows/test-action.yml index aa809aec..01260d42 100644 --- a/.github/workflows/test-action.yml +++ b/.github/workflows/test-action.yml @@ -60,9 +60,10 @@ jobs: echo "::error::Version mismatch! Expected v0.7.0, got ${{ steps.setup-awf.outputs.version }}" exit 1 fi - # Verify image tag is set correctly (without 'v' prefix) - if [[ "${{ steps.setup-awf.outputs.image-tag }}" != "0.7.0" ]]; then - echo "::error::Image tag mismatch! Expected 0.7.0, got ${{ steps.setup-awf.outputs.image-tag }}" + # Verify image tag metadata starts with the expected base tag (without 'v' prefix) + # and may include optional digest metadata entries. + if [[ "${{ steps.setup-awf.outputs.image-tag }}" != 0.7.0* ]]; then + echo "::error::Image tag metadata mismatch! Expected prefix 0.7.0, got ${{ steps.setup-awf.outputs.image-tag }}" exit 1 fi diff --git a/action.yml b/action.yml index 7de08dfe..fd464dab 100644 --- a/action.yml +++ b/action.yml @@ -20,7 +20,7 @@ outputs: description: 'The version of awf that was installed' value: ${{ steps.install.outputs.version }} image-tag: - description: 'The image tag that matches the installed version (without the v prefix)' + description: 'The image tag metadata for awf runtime images (base tag plus optional per-image digests)' value: ${{ steps.install.outputs.image_tag }} runs: @@ -99,14 +99,15 @@ runs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" - # Extract image tag (version without 'v' prefix) - IMAGE_TAG="${VERSION#v}" - echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT" - # Download URLs BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" BINARY_URL="${BASE_URL}/${BINARY_NAME}" CHECKSUMS_URL="${BASE_URL}/checksums.txt" + CONTAINERS_URL="${BASE_URL}/containers.txt" + + # Extract image tag (version without 'v' prefix), then augment with digest pins when available + IMAGE_TAG="${VERSION#v}" + IMAGE_TAG_WITH_DIGESTS="$IMAGE_TAG" # Download binary echo "Downloading awf ${VERSION}..." @@ -122,6 +123,42 @@ runs: exit 1 fi + # Download optional containers digest manifest + echo "Downloading containers manifest (optional)..." + if curl -fsSL "$CONTAINERS_URL" -o "$INSTALL_DIR/containers.txt"; then + extract_digest() { + local image_name="$1" + grep -E "^ghcr\\.io/${REPO}/${image_name}@sha256:[a-f0-9]{64}$" "$INSTALL_DIR/containers.txt" \ + | sed -E 's#.*@(sha256:[a-f0-9]{64})#\1#' \ + | head -n 1 + } + + DIGEST_ENTRIES=() + SQUID_DIGEST="$(extract_digest squid || true)" + AGENT_DIGEST="$(extract_digest agent || true)" + AGENT_ACT_DIGEST="$(extract_digest agent-act || true)" + API_PROXY_DIGEST="$(extract_digest api-proxy || true)" + CLI_PROXY_DIGEST="$(extract_digest cli-proxy || true)" + + [ -n "${SQUID_DIGEST:-}" ] && DIGEST_ENTRIES+=("squid=${SQUID_DIGEST}") + [ -n "${AGENT_DIGEST:-}" ] && DIGEST_ENTRIES+=("agent=${AGENT_DIGEST}") + [ -n "${AGENT_ACT_DIGEST:-}" ] && DIGEST_ENTRIES+=("agent-act=${AGENT_ACT_DIGEST}") + [ -n "${API_PROXY_DIGEST:-}" ] && DIGEST_ENTRIES+=("api-proxy=${API_PROXY_DIGEST}") + [ -n "${CLI_PROXY_DIGEST:-}" ] && DIGEST_ENTRIES+=("cli-proxy=${CLI_PROXY_DIGEST}") + + if [ "${#DIGEST_ENTRIES[@]}" -gt 0 ]; then + DIGEST_CSV="$(IFS=,; echo "${DIGEST_ENTRIES[*]}")" + IMAGE_TAG_WITH_DIGESTS="${IMAGE_TAG},${DIGEST_CSV}" + echo "Discovered digest pins for ${#DIGEST_ENTRIES[@]} image(s)" + else + echo "::warning::containers.txt found but no valid digest entries were parsed; falling back to tag-only image metadata" + fi + else + echo "::warning::No containers.txt found for ${VERSION}; falling back to tag-only image metadata" + fi + + echo "image_tag=$IMAGE_TAG_WITH_DIGESTS" >> "$GITHUB_OUTPUT" + # Verify checksum echo "Verifying SHA256 checksum..." @@ -166,8 +203,8 @@ runs: # Make executable chmod +x "$INSTALL_DIR/awf" - # Clean up checksums file - rm -f "$INSTALL_DIR/checksums.txt" + # Clean up downloaded metadata files + rm -f "$INSTALL_DIR/checksums.txt" "$INSTALL_DIR/containers.txt" # Add to PATH echo "$INSTALL_DIR" >> "$GITHUB_PATH" @@ -184,20 +221,30 @@ runs: set -euo pipefail REGISTRY="ghcr.io/github/gh-aw-firewall" + + BASE_TAG="${IMAGE_TAG%%,*}" + extract_digest_from_tag() { + local key="$1" + echo "$IMAGE_TAG" | tr ',' '\n' | grep -E "^${key}=sha256:[a-f0-9]{64}$" | cut -d'=' -f2 | head -n 1 + } + SQUID_DIGEST="$(extract_digest_from_tag squid || true)" + AGENT_DIGEST="$(extract_digest_from_tag agent || true)" + SQUID_REF="${REGISTRY}/squid:${BASE_TAG}${SQUID_DIGEST:+@${SQUID_DIGEST}}" + AGENT_REF="${REGISTRY}/agent:${BASE_TAG}${AGENT_DIGEST:+@${AGENT_DIGEST}}" echo "Pulling awf Docker images with tag: ${IMAGE_TAG}" # Pull squid image - echo "Pulling ${REGISTRY}/squid:${IMAGE_TAG}..." - if ! docker pull "${REGISTRY}/squid:${IMAGE_TAG}"; then - echo "::warning::Failed to pull squid image with tag ${IMAGE_TAG}, trying 'latest'" + echo "Pulling ${SQUID_REF}..." + if ! docker pull "${SQUID_REF}"; then + echo "::warning::Failed to pull squid image ${SQUID_REF}, trying 'latest'" docker pull "${REGISTRY}/squid:latest" fi # Pull agent image - echo "Pulling ${REGISTRY}/agent:${IMAGE_TAG}..." - if ! docker pull "${REGISTRY}/agent:${IMAGE_TAG}"; then - echo "::warning::Failed to pull agent image with tag ${IMAGE_TAG}, trying 'latest'" + echo "Pulling ${AGENT_REF}..." + if ! docker pull "${AGENT_REF}"; then + echo "::warning::Failed to pull agent image ${AGENT_REF}, trying 'latest'" docker pull "${REGISTRY}/agent:latest" fi diff --git a/docs/github_actions.md b/docs/github_actions.md index abd095ac..adb8896f 100644 --- a/docs/github_actions.md +++ b/docs/github_actions.md @@ -36,7 +36,7 @@ The action: | Output | Description | |--------|-------------| | `version` | The version that was installed (e.g., `v0.7.0`) | -| `image-tag` | The image tag matching the version (e.g., `0.7.0`) | +| `image-tag` | Image tag metadata for runtime containers. Format: `0.7.0` or `0.7.0,squid=sha256:...,agent=sha256:...,api-proxy=sha256:...` | #### Pinning Docker Image Versions diff --git a/docs/usage.md b/docs/usage.md index 0f0b16b1..fbafe01e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -36,9 +36,11 @@ Options: ghcr.io/catthehacker/ubuntu:full-XX.XX --image-registry Container image registry (default: ghcr.io/github/gh-aw-firewall) --image-tag Container image tag (default: latest) - Image name varies by --agent-image preset: - default → agent: - act → agent-act: + Optional digest metadata: + ,squid=sha256:...,agent=sha256:...,api-proxy=sha256:... + Image name varies by --agent-image preset: + default → agent: + act → agent-act: --skip-pull Use local images without pulling from registry (requires images to be pre-downloaded) (default: false) -e, --env Additional environment variables to pass to container (can be diff --git a/src/cli.ts b/src/cli.ts index 314b934a..184df937 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1370,6 +1370,8 @@ program .option( '--image-tag ', 'Container image tag (applies to both squid and agent images)\n' + + ' Optional digest metadata format:\n' + + ' ,squid=sha256:...,agent=sha256:...,api-proxy=sha256:...\n' + ' Image name varies by --agent-image preset:\n' + ' default → agent:\n' + ' act → agent-act:', @@ -2270,7 +2272,11 @@ program 'Container image registry', 'ghcr.io/github/gh-aw-firewall' ) - .option('--image-tag ', 'Container image tag (applies to squid, agent, and api-proxy images)', 'latest') + .option( + '--image-tag ', + 'Container image tag. Supports optional digest metadata: ,squid=sha256:...,agent=sha256:...,api-proxy=sha256:...', + 'latest' + ) .option( '--agent-image ', 'Agent image preset (default, act) or custom image', diff --git a/src/commands/predownload.test.ts b/src/commands/predownload.test.ts index fde8187c..2f9c2226 100644 --- a/src/commands/predownload.test.ts +++ b/src/commands/predownload.test.ts @@ -74,6 +74,27 @@ describe('predownload', () => { ]); }); + it('should append per-image digests from image-tag metadata', () => { + const images = resolveImages({ + ...defaults, + imageTag: [ + '0.25.18', + 'squid=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'agent=sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'api-proxy=sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + 'cli-proxy=sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + ].join(','), + enableApiProxy: true, + difcProxy: true, + }); + expect(images).toEqual([ + 'ghcr.io/github/gh-aw-firewall/squid:0.25.18@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'ghcr.io/github/gh-aw-firewall/agent:0.25.18@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + 'ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.18@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + ]); + }); + it('should use custom agent image as-is', () => { const images = resolveImages({ ...defaults, agentImage: 'ubuntu:22.04' }); expect(images).toEqual([ @@ -93,6 +114,12 @@ describe('predownload', () => { 'must not contain whitespace', ); }); + + it('should reject invalid image-tag digest metadata', () => { + expect(() => + resolveImages({ ...defaults, imageTag: '0.25.18,squid=sha256:not-a-real-digest' }) + ).toThrow('Invalid --image-tag digest'); + }); }); describe('predownloadCommand', () => { diff --git a/src/commands/predownload.ts b/src/commands/predownload.ts index b9ac8a87..9ae958ce 100644 --- a/src/commands/predownload.ts +++ b/src/commands/predownload.ts @@ -1,5 +1,6 @@ import execa from 'execa'; import { logger } from '../logger'; +import { buildRuntimeImageRef, parseImageTag } from '../image-tag'; export interface PredownloadOptions { imageRegistry: string; @@ -27,16 +28,17 @@ function validateImageReference(image: string): void { */ export function resolveImages(options: PredownloadOptions): string[] { const { imageRegistry, imageTag, agentImage, enableApiProxy } = options; + const parsedImageTag = parseImageTag(imageTag); const images: string[] = []; // Always pull squid - images.push(`${imageRegistry}/squid:${imageTag}`); + images.push(buildRuntimeImageRef(imageRegistry, 'squid', parsedImageTag)); // Pull agent image based on preset const isPreset = agentImage === 'default' || agentImage === 'act'; if (isPreset) { const imageName = agentImage === 'act' ? 'agent-act' : 'agent'; - images.push(`${imageRegistry}/${imageName}:${imageTag}`); + images.push(buildRuntimeImageRef(imageRegistry, imageName, parsedImageTag)); } else { // Custom image - validate and pull as-is validateImageReference(agentImage); @@ -45,12 +47,12 @@ export function resolveImages(options: PredownloadOptions): string[] { // Optionally pull api-proxy if (enableApiProxy) { - images.push(`${imageRegistry}/api-proxy:${imageTag}`); + images.push(buildRuntimeImageRef(imageRegistry, 'api-proxy', parsedImageTag)); } // Optionally pull cli-proxy (mcpg is now started externally by the compiler) if (options.difcProxy) { - images.push(`${imageRegistry}/cli-proxy:${imageTag}`); + images.push(buildRuntimeImageRef(imageRegistry, 'cli-proxy', parsedImageTag)); } return images; diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 2b1bbb7d..659d2318 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -486,6 +486,37 @@ describe('docker-manager', () => { expect(result.services.agent.build).toBeUndefined(); }); + it('should append per-image digests from image-tag metadata', () => { + const customConfig = { + ...mockConfig, + enableApiProxy: true, + imageTag: [ + 'v1.0.0', + 'squid=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'agent=sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'api-proxy=sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + ].join(','), + }; + const networkWithProxy = { + ...mockNetworkConfig, + proxyIp: '172.30.0.30', + }; + const result = generateDockerCompose(customConfig, networkWithProxy); + + expect(result.services['squid-proxy'].image).toBe( + 'ghcr.io/github/gh-aw-firewall/squid:v1.0.0@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + ); + expect(result.services.agent.image).toBe( + 'ghcr.io/github/gh-aw-firewall/agent:v1.0.0@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + ); + expect(result.services['iptables-init'].image).toBe( + 'ghcr.io/github/gh-aw-firewall/agent:v1.0.0@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + ); + expect(result.services['api-proxy'].image).toBe( + 'ghcr.io/github/gh-aw-firewall/api-proxy:v1.0.0@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' + ); + }); + it('should build locally with custom catthehacker full image', () => { const customConfig = { ...mockConfig, diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 3fefd8f8..b127c59e 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -9,6 +9,7 @@ import { generateSquidConfig, generatePolicyManifest } from './squid-config'; import { generateSessionCa, initSslDb, CaFiles, parseUrlPatterns, cleanupSslKeyMaterial, unmountSslTmpfs } from './ssl-bump'; import { DEFAULT_DNS_SERVERS } from './dns-resolver'; import { PROXY_ENV_VARS } from './upstream-proxy'; +import { parseImageTag, buildRuntimeImageRef } from './image-tag'; const SQUID_PORT = 3128; @@ -618,7 +619,7 @@ export function generateDockerCompose( // Default to GHCR images unless buildLocal is explicitly set const useGHCR = !config.buildLocal; const registry = config.imageRegistry || 'ghcr.io/github/gh-aw-firewall'; - const tag = config.imageTag || 'latest'; + const parsedImageTag = parseImageTag(config.imageTag || 'latest'); // Squid logs path: use proxyLogsDir if specified (direct write), otherwise workDir/squid-logs const squidLogsPath = config.proxyLogsDir || `${config.workDir}/squid-logs`; @@ -726,7 +727,7 @@ export function generateDockerCompose( // Use GHCR image or build locally // For SSL Bump, we always build locally to include OpenSSL tools if (useGHCR && !config.sslBump) { - squidService.image = `${registry}/squid:${tag}`; + squidService.image = buildRuntimeImageRef(registry, 'squid', parsedImageTag); } else { squidService.build = { context: path.join(projectRoot, 'containers/squid'), @@ -1590,8 +1591,8 @@ export function generateDockerCompose( // Use pre-built GHCR image for preset images // The GHCR images already have the necessary setup for chroot mode const imageName = agentImage === 'act' ? 'agent-act' : 'agent'; - agentService.image = `${registry}/${imageName}:${tag}`; - logger.debug(`Using GHCR image ${imageName}:${tag}`); + agentService.image = buildRuntimeImageRef(registry, imageName, parsedImageTag); + logger.debug(`Using GHCR image ${agentService.image}`); } else if (config.buildLocal || !isPreset) { // Build locally when: // 1. --build-local is explicitly specified, OR @@ -1781,7 +1782,7 @@ export function generateDockerCompose( // Use GHCR image or build locally if (useGHCR) { - proxyService.image = `${registry}/api-proxy:${tag}`; + proxyService.image = buildRuntimeImageRef(registry, 'api-proxy', parsedImageTag); } else { proxyService.build = { context: path.join(projectRoot, 'containers/api-proxy'), @@ -1993,7 +1994,7 @@ export function generateDockerCompose( // Use GHCR image or build locally for the Node.js HTTP server container if (useGHCR) { - cliProxyService.image = `${registry}/cli-proxy:${tag}`; + cliProxyService.image = buildRuntimeImageRef(registry, 'cli-proxy', parsedImageTag); } else { cliProxyService.build = { context: path.join(projectRoot, 'containers/cli-proxy'), diff --git a/src/image-tag.ts b/src/image-tag.ts new file mode 100644 index 00000000..b491cdde --- /dev/null +++ b/src/image-tag.ts @@ -0,0 +1,71 @@ +export const IMAGE_DIGEST_KEYS = ['squid', 'agent', 'agent-act', 'api-proxy', 'cli-proxy'] as const; + +export type ImageDigestKey = typeof IMAGE_DIGEST_KEYS[number]; + +export interface ParsedImageTag { + tag: string; + digests: Partial>; +} + +/** + * Parse image-tag values in either of these formats: + * - legacy: "0.25.18" + * - digest-aware: "0.25.18,squid=sha256:...,agent=sha256:...,api-proxy=sha256:..." + */ +export function parseImageTag(imageTag: string): ParsedImageTag { + const raw = imageTag.trim(); + if (!raw) { + throw new Error('Invalid --image-tag value: tag cannot be empty'); + } + + const [rawTag, ...rawDigestEntries] = raw.split(','); + const tag = rawTag.trim(); + if (!tag) { + throw new Error('Invalid --image-tag value: tag cannot be empty'); + } + + const digests: Partial> = {}; + const validKeys = new Set(IMAGE_DIGEST_KEYS); + + for (const entry of rawDigestEntries) { + const trimmedEntry = entry.trim(); + if (!trimmedEntry) { + continue; + } + + const equalIndex = trimmedEntry.indexOf('='); + if (equalIndex <= 0 || equalIndex === trimmedEntry.length - 1) { + throw new Error( + `Invalid --image-tag digest entry "${trimmedEntry}". Expected format: =sha256:<64-hex>` + ); + } + + const key = trimmedEntry.slice(0, equalIndex).trim(); + const digest = trimmedEntry.slice(equalIndex + 1).trim(); + + if (!validKeys.has(key)) { + throw new Error( + `Invalid --image-tag digest key "${key}". Supported keys: ${IMAGE_DIGEST_KEYS.join(', ')}` + ); + } + + if (!/^sha256:[a-f0-9]{64}$/.test(digest)) { + throw new Error( + `Invalid --image-tag digest "${digest}" for "${key}". Expected sha256:<64 lowercase hex>` + ); + } + + digests[key as ImageDigestKey] = digest; + } + + return { tag, digests }; +} + +export function buildRuntimeImageRef( + imageRegistry: string, + imageName: string, + parsedTag: ParsedImageTag +): string { + const digest = parsedTag.digests[imageName as ImageDigestKey]; + return `${imageRegistry}/${imageName}:${parsedTag.tag}${digest ? `@${digest}` : ''}`; +} From c2301b3ad251ad9a7a98d4892ab0bc65ad6ef44a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:58:50 +0000 Subject: [PATCH 3/8] chore: clarify setup action digest fallback warning Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/7e6bdecd-3b0f-43b9-b54e-22e09e83c585 --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index fd464dab..55b1a6d1 100644 --- a/action.yml +++ b/action.yml @@ -151,7 +151,7 @@ runs: IMAGE_TAG_WITH_DIGESTS="${IMAGE_TAG},${DIGEST_CSV}" echo "Discovered digest pins for ${#DIGEST_ENTRIES[@]} image(s)" else - echo "::warning::containers.txt found but no valid digest entries were parsed; falling back to tag-only image metadata" + echo "::warning::containers.txt was downloaded, but no valid digest entries matched ghcr.io/${REPO}/@sha256:; file may be malformed or from an unexpected repository format. Falling back to tag-only image metadata." fi else echo "::warning::No containers.txt found for ${VERSION}; falling back to tag-only image metadata" From deca52d647e881e6a3af553383c2ae56207d323b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:00:06 +0000 Subject: [PATCH 4/8] chore: polish digest validation messages Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/7e6bdecd-3b0f-43b9-b54e-22e09e83c585 --- action.yml | 3 ++- src/image-tag.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 55b1a6d1..f86bbcc2 100644 --- a/action.yml +++ b/action.yml @@ -151,7 +151,8 @@ runs: IMAGE_TAG_WITH_DIGESTS="${IMAGE_TAG},${DIGEST_CSV}" echo "Discovered digest pins for ${#DIGEST_ENTRIES[@]} image(s)" else - echo "::warning::containers.txt was downloaded, but no valid digest entries matched ghcr.io/${REPO}/@sha256:; file may be malformed or from an unexpected repository format. Falling back to tag-only image metadata." + echo "::warning::containers.txt downloaded but no valid digest entries were parsed." + echo "::warning::Expected format: ghcr.io/${REPO}/@sha256:. Falling back to tag-only image metadata." fi else echo "::warning::No containers.txt found for ${VERSION}; falling back to tag-only image metadata" diff --git a/src/image-tag.ts b/src/image-tag.ts index b491cdde..ac5dab98 100644 --- a/src/image-tag.ts +++ b/src/image-tag.ts @@ -51,7 +51,7 @@ export function parseImageTag(imageTag: string): ParsedImageTag { if (!/^sha256:[a-f0-9]{64}$/.test(digest)) { throw new Error( - `Invalid --image-tag digest "${digest}" for "${key}". Expected sha256:<64 lowercase hex>` + `Invalid --image-tag digest "${digest}" for "${key}". Expected lowercase sha256:<64-hex>` ); } From 83e0f32daacafdeef887816d5b0ff0aa28d0b30d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:02:03 +0000 Subject: [PATCH 5/8] chore: harden digest parsing robustness Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/7e6bdecd-3b0f-43b9-b54e-22e09e83c585 --- action.yml | 10 +++++----- src/image-tag.ts | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/action.yml b/action.yml index f86bbcc2..48312217 100644 --- a/action.yml +++ b/action.yml @@ -128,8 +128,9 @@ runs: if curl -fsSL "$CONTAINERS_URL" -o "$INSTALL_DIR/containers.txt"; then extract_digest() { local image_name="$1" - grep -E "^ghcr\\.io/${REPO}/${image_name}@sha256:[a-f0-9]{64}$" "$INSTALL_DIR/containers.txt" \ - | sed -E 's#.*@(sha256:[a-f0-9]{64})#\1#' \ + grep -E "^ghcr\\.io/${REPO}/${image_name}@sha256:[a-fA-F0-9]{64}$" "$INSTALL_DIR/containers.txt" \ + | sed -E 's#.*@(sha256:[a-fA-F0-9]{64})#\1#' \ + | tr '[:upper:]' '[:lower:]' \ | head -n 1 } @@ -151,8 +152,7 @@ runs: IMAGE_TAG_WITH_DIGESTS="${IMAGE_TAG},${DIGEST_CSV}" echo "Discovered digest pins for ${#DIGEST_ENTRIES[@]} image(s)" else - echo "::warning::containers.txt downloaded but no valid digest entries were parsed." - echo "::warning::Expected format: ghcr.io/${REPO}/@sha256:. Falling back to tag-only image metadata." + echo "::warning::containers.txt downloaded but no valid digest entries matched ghcr.io/${REPO}/@sha256:; falling back to tag-only image metadata." fi else echo "::warning::No containers.txt found for ${VERSION}; falling back to tag-only image metadata" @@ -226,7 +226,7 @@ runs: BASE_TAG="${IMAGE_TAG%%,*}" extract_digest_from_tag() { local key="$1" - echo "$IMAGE_TAG" | tr ',' '\n' | grep -E "^${key}=sha256:[a-f0-9]{64}$" | cut -d'=' -f2 | head -n 1 + echo "$IMAGE_TAG" | tr ',' '\n' | grep -E "^${key}=sha256:[a-fA-F0-9]{64}$" | cut -d'=' -f2 | tr '[:upper:]' '[:lower:]' | head -n 1 } SQUID_DIGEST="$(extract_digest_from_tag squid || true)" AGENT_DIGEST="$(extract_digest_from_tag agent || true)" diff --git a/src/image-tag.ts b/src/image-tag.ts index ac5dab98..c6753db1 100644 --- a/src/image-tag.ts +++ b/src/image-tag.ts @@ -66,6 +66,12 @@ export function buildRuntimeImageRef( imageName: string, parsedTag: ParsedImageTag ): string { + if (!IMAGE_DIGEST_KEYS.includes(imageName as ImageDigestKey)) { + throw new Error( + `Invalid runtime image name "${imageName}". Supported names: ${IMAGE_DIGEST_KEYS.join(', ')}` + ); + } + const digest = parsedTag.digests[imageName as ImageDigestKey]; return `${imageRegistry}/${imageName}:${parsedTag.tag}${digest ? `@${digest}` : ''}`; } From fcb27aa85d3aacbd5bd5a187ba641328c6ff2807 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sat, 18 Apr 2026 16:05:05 -0700 Subject: [PATCH 6/8] Update docs/github_actions.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/github_actions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/github_actions.md b/docs/github_actions.md index adb8896f..60f00388 100644 --- a/docs/github_actions.md +++ b/docs/github_actions.md @@ -36,7 +36,7 @@ The action: | Output | Description | |--------|-------------| | `version` | The version that was installed (e.g., `v0.7.0`) | -| `image-tag` | Image tag metadata for runtime containers. Format: `0.7.0` or `0.7.0,squid=sha256:...,agent=sha256:...,api-proxy=sha256:...` | +| `image-tag` | Image tag metadata for runtime containers. Format: `0.7.0` or `0.7.0,squid=sha256:...,agent=sha256:...,api-proxy=sha256:...,agent-act=sha256:...,cli-proxy=sha256:...`. Supported digest keys currently include `squid`, `agent`, `api-proxy`, `agent-act`, and `cli-proxy`; additional keys may appear in future releases. | #### Pinning Docker Image Versions From d0c98fcd81f410f70f47ebb088a91ce723121cb1 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sat, 18 Apr 2026 16:05:24 -0700 Subject: [PATCH 7/8] Update docs/usage.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/usage.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index fbafe01e..a0f2ea3f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -37,10 +37,11 @@ Options: --image-registry Container image registry (default: ghcr.io/github/gh-aw-firewall) --image-tag Container image tag (default: latest) Optional digest metadata: - ,squid=sha256:...,agent=sha256:...,api-proxy=sha256:... - Image name varies by --agent-image preset: - default → agent: - act → agent-act: + ,squid=sha256:...,agent=sha256:...,agent-act=sha256:...,api-proxy=sha256:...,cli-proxy=sha256:... + Supported digest metadata keys: squid, agent, agent-act, api-proxy, cli-proxy + Image name varies by --agent-image preset: + default → agent: + act → agent-act: --skip-pull Use local images without pulling from registry (requires images to be pre-downloaded) (default: false) -e, --env Additional environment variables to pass to container (can be From 856dfdaf4ccdde607f45baf721ce7c57140b21f4 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sat, 18 Apr 2026 16:05:36 -0700 Subject: [PATCH 8/8] Update src/cli.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/cli.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 184df937..71709644 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1369,9 +1369,9 @@ program ) .option( '--image-tag ', - 'Container image tag (applies to both squid and agent images)\n' + + 'Container image tag (applies to squid, agent/agent-act, api-proxy, and cli-proxy when enabled)\n' + ' Optional digest metadata format:\n' + - ' ,squid=sha256:...,agent=sha256:...,api-proxy=sha256:...\n' + + ' ,squid=sha256:...,agent=sha256:...,agent-act=sha256:...,api-proxy=sha256:...,cli-proxy=sha256:...\n' + ' Image name varies by --agent-image preset:\n' + ' default → agent:\n' + ' act → agent-act:',