Repository navigation
Honor container image overrides in threat-detection jobs #63014
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5fec81d
9506fe1
f4331c2
add3e9f
beb2628
476c477
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # ADR-63014: Honor detection job container overrides | ||
|
|
||
| **Date**: 2026-09-23 | ||
| **Status**: Draft | ||
| **Deciders**: gh-aw maintainers | ||
|
|
||
| --- | ||
|
|
||
| ### Context | ||
|
|
||
| Threat-detection jobs are derived from the parent workflow configuration, but they previously dropped `sandbox.agent.images` and `container_pins` while building the detection-specific workflow data. The PR description and diff show that this caused detection jobs to fall back to the default `ghcr.io` AWF registry even when the main agent workflow had been configured to use mirrored or private container images. This created inconsistent runtime behavior between the main workflow and threat-detection jobs and could break repositories that require private-registry routing or pinned mirrored images. The change is focused on how detection-job workflow data inherits container-related configuration from the parent workflow. | ||
|
|
||
| ### Decision | ||
|
|
||
| We will propagate `sandbox.agent.images` and `container_pins` into threat-detection workflow data and clone both maps before assigning them to the derived detection configuration. When `container_pins` redirects a required default AWF image, the detection job will translate the mappings into a complete `container.images` runtime manifest. Threat-detection jobs will therefore use the same image-role overrides and repository container-pin mappings as the parent workflow during both pre-pull and runtime execution. We chose this because the PR evidence shows the architectural intent is configuration consistency across workflow variants, while cloning prevents the derived detection configuration from mutating the parent workflow state. | ||
|
|
||
| ### Alternatives Considered | ||
|
|
||
| #### Alternative 1: Keep threat-detection jobs on the default AWF registry | ||
|
|
||
| This preserves the previous behavior and avoids carrying more configuration into derived detection workflow data. It was not chosen because the PR shows that defaulting to `ghcr.io` breaks expected parity with the parent workflow and ignores explicit private-registry or mirrored-image configuration already supplied by repository authors. | ||
|
|
||
| #### Alternative 2: Propagate overrides by sharing the parent maps directly | ||
|
|
||
| This would let detection jobs see the same image and pin data with less copying logic. It was not chosen because the tests in this PR explicitly validate that detection workflow data must not alias the parent maps; sharing references would make later mutation in derived workflow construction able to leak back into the parent configuration. | ||
|
|
||
| ### Consequences | ||
|
|
||
| #### Positive | ||
| - Threat-detection jobs now honor the same mirrored or private container image configuration as the primary workflow jobs. | ||
| - Detection pre-pull steps use mapped container references instead of silently falling back to the default AWF registry. | ||
| - Detection runtime configuration uses mapped references instead of starting default-registry images. | ||
| - Cloning the image and pin mappings preserves isolation between parent workflow data and derived detection workflow data. | ||
|
|
||
| #### Negative | ||
| - The threat-detection workflow builder now has extra configuration-copying logic that must stay synchronized with future sandbox configuration changes. | ||
| - Additional tests and cloning introduce a small maintenance cost whenever container override behavior evolves. | ||
|
|
||
| #### Neutral | ||
| - The change does not introduce a new registry-selection feature; it extends existing override mechanisms into detection jobs. | ||
| - Runtime behavior only changes for workflows that already set `sandbox.agent.images` or `container_pins`; workflows using defaults continue to behave the same. | ||
|
|
||
| --- | ||
|
|
||
| *ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package workflow | |
|
|
||
| import ( | ||
| "fmt" | ||
| "maps" | ||
| "path" | ||
| "slices" | ||
| "strconv" | ||
|
|
@@ -115,21 +116,23 @@ func buildThreatDetectionWorkflowData(data *WorkflowData, engineID string) *Work | |
| } | ||
|
|
||
| detectionData := &WorkflowData{ | ||
| AI: engineID, | ||
| ActionCache: data.ActionCache, | ||
| Features: data.Features, | ||
| Jobs: data.Jobs, | ||
| Permissions: data.Permissions, | ||
| ParsedFrontmatter: data.ParsedFrontmatter, | ||
| CachedPermissions: data.CachedPermissions, | ||
| ModelCosts: data.ModelCosts, | ||
| IsDetectionRun: true, | ||
| RunnerConfig: data.RunnerConfig, | ||
| TimeoutMinutes: "timeout-minutes: " + resolveDetectionJobTimeoutValue(data), | ||
| CompiledVersion: data.CompiledVersion, | ||
| AI: engineID, | ||
| ActionCache: data.ActionCache, | ||
| Features: data.Features, | ||
| Jobs: data.Jobs, | ||
| Permissions: data.Permissions, | ||
| ParsedFrontmatter: data.ParsedFrontmatter, | ||
| CachedPermissions: data.CachedPermissions, | ||
| ModelCosts: data.ModelCosts, | ||
| IsDetectionRun: true, | ||
| RunnerConfig: data.RunnerConfig, | ||
| TimeoutMinutes: "timeout-minutes: " + resolveDetectionJobTimeoutValue(data), | ||
| CompiledVersion: data.CompiledVersion, | ||
| ContainerPinMappings: maps.Clone(data.ContainerPinMappings), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed by test: I reproduced this concretely (added a scratch test, not committed): setting "container":{"imageTag":"0.28.23,squid=sha256:...,agent=sha256:...,api-proxy=sha256:...,cli-proxy=sha256:..."}No
So this PR fixes the Suggest either:
@copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in |
||
| SandboxConfig: &SandboxConfig{ | ||
| Agent: &AgentSandboxConfig{ | ||
| Type: SandboxTypeAWF, | ||
| Type: SandboxTypeAWF, | ||
| Images: maps.Clone(getSandboxAgentImages(data)), | ||
| }, | ||
| }, | ||
| } | ||
|
|
@@ -145,10 +148,42 @@ func buildThreatDetectionWorkflowData(data *WorkflowData, engineID string) *Work | |
| } | ||
| detectionData.SandboxConfig.Agent.Version = firewallCopy.Version | ||
| } | ||
| if len(detectionData.SandboxConfig.Agent.Images) == 0 { | ||
| detectionData.SandboxConfig.Agent.Images = buildThreatDetectionContainerImages(detectionData) | ||
| } | ||
|
|
||
| return detectionData | ||
| } | ||
|
|
||
| // buildThreatDetectionContainerImages translates AWF container pin redirects into | ||
| // the closed runtime manifest expected by AWF. A complete manifest is emitted only | ||
| // when at least one required AWF role is redirected. | ||
| func buildThreatDetectionContainerImages(data *WorkflowData) map[string]string { | ||
| if len(data.ContainerPinMappings) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| imageTag := getAWFImageTag(getFirewallConfig(data)) | ||
| images := make(map[string]string) | ||
| hasMappedImage := false | ||
| for _, role := range requiredAWFImageRoles(data) { | ||
| image := defaultAWFImageForRole(role, imageTag) | ||
| if applyContainerPinMappingFromData(image, data) != image { | ||
| hasMappedImage = true | ||
| } | ||
| resolved := resolveContainerImage(image, data) | ||
| if !awfPinnedImagePattern.MatchString(resolved) { | ||
| threatLog.Printf("Skipping detection container manifest: %s image is not digest-pinned", role) | ||
| return nil | ||
| } | ||
| images[role] = resolved | ||
| } | ||
| if !hasMappedImage { | ||
| return nil | ||
| } | ||
| return images | ||
| } | ||
|
|
||
| // buildPullAWFContainersStep creates a step that pre-pulls AWF (agent workflow firewall) | ||
| // container images in the detection job. The detection engine runs inside AWF, which uses | ||
| // the firewall stack containers needed for the selected topology (squid, agent, api-proxy, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in
beb2628:buildThreatDetectionWorkflowDatanow callsbuildThreatDetectionContainerImages, which translatesContainerPinMappingsinto a closedcontainer.imagesmanifest (only when all required AWF roles resolve to digest-pinned mirrors).BuildAWFConfigJSONtherefore emitscontainer.imagesinstead ofcontainer.imageTagforcontainer_pins-only workflows, so the detection sandbox starts the mirrored image at runtime, notghcr.io.TestBuildThreatDetectionAWFConfigUsesContainerPinMappingsasserts the generated AWF config JSON directly (not just the pre-pull step).