From 05c5d22800b5dd1d46ac0a6a8e27f29e2d3cfd0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:48:34 +0000 Subject: [PATCH 1/8] Initial plan From 89e9be29cddf7857ada4aa177a5a531defa1cf9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:05:05 +0000 Subject: [PATCH 2/8] chore: start threat detection follow-up fix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/sighthound-security-scan.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sighthound-security-scan.lock.yml b/.github/workflows/sighthound-security-scan.lock.yml index 1fa6e597a0a..8d42c68ef95 100644 --- a/.github/workflows/sighthound-security-scan.lock.yml +++ b/.github/workflows/sighthound-security-scan.lock.yml @@ -1454,7 +1454,7 @@ jobs: export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.30/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.30,squid=sha256:eb74fca5309c7542df0f32aef41b92c728ecef7ac3b0cca9f9a6b97cc324d22a,agent=sha256:3cc1e14efa9e52ed1fc29d72a0eabf02ff86b30c6af9685fa9ddd687caca6613,agent-act=sha256:d0beee47dd38e2df577d15b9ddf763a2b771367326fecb02e1aa924bc395954a,api-proxy=sha256:fdbd94bb668ed736a27c146633842db5c7e658dc7a0d6a0e6011e74e18132785,cli-proxy=sha256:959c876217038ac6f9c2047b591e819e5f1f726625a34490ccdcacaa71d83e4c\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.31/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.31,squid=sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494,agent=sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1,agent-act=sha256:58fee05c1c54ba5ca1e7056b3aaea30281841d5899093002e2c650710c50540f,api-proxy=sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63,cli-proxy=sha256:7c63bc4e57d6eac1be996bb793a5a2d74d40b15a616003f4b6805a457046c673\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" From 15b7967eca5ef70a3e1d638a053f6abc5df7744b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:12:45 +0000 Subject: [PATCH 3/8] fix: propagate detection pull-step runner topology Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/threat_detection_external.go | 100 +++++++++--------- .../threat_detection_inline_engine.go | 32 ++---- pkg/workflow/threat_detection_test.go | 40 +++++++ 3 files changed, 96 insertions(+), 76 deletions(-) diff --git a/pkg/workflow/threat_detection_external.go b/pkg/workflow/threat_detection_external.go index faa824a4544..05d7a91d1d1 100644 --- a/pkg/workflow/threat_detection_external.go +++ b/pkg/workflow/threat_detection_external.go @@ -66,10 +66,37 @@ func codexProxyWebsocketBaseURL(apiBase string) string { } } +// buildThreatDetectionWorkflowData creates the shared minimal WorkflowData used by +// detection-job helper steps so topology- and feature-dependent behavior stays in sync. +func buildThreatDetectionWorkflowData(data *WorkflowData, engineID string) *WorkflowData { + if engineID == "" { + engineID = data.AI + } + if engineID == "" { + engineID = "claude" + } + + return &WorkflowData{ + AI: engineID, + ActionCache: data.ActionCache, + Features: data.Features, + Permissions: data.Permissions, + CachedPermissions: data.CachedPermissions, + IsDetectionRun: true, + RunnerConfig: data.RunnerConfig, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Type: SandboxTypeAWF, + }, + }, + } +} + // 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 -// three containers (squid, agent, api-proxy). Pre-pulling avoids on-demand pulls at runtime. -// Only AWF images are pulled here; MCP server images are not needed for detection. +// the firewall stack containers needed for the selected topology (squid, agent, api-proxy, +// plus build-tools on arc-dind). Pre-pulling avoids on-demand pulls at runtime. Only AWF +// images are pulled here; MCP server images are not needed for detection. func (c *Compiler) buildPullAWFContainersStep(data *WorkflowData) []string { // Build a minimal WorkflowData that represents the detection engine context so // collectDockerImages returns only the AWF firewall images (no MCP tool images). @@ -77,17 +104,8 @@ func (c *Compiler) buildPullAWFContainersStep(data *WorkflowData) []string { if engineSetting == "" { engineSetting = "claude" } - detectionData := &WorkflowData{ - Tools: map[string]any{}, - AI: engineSetting, - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Type: SandboxTypeAWF, - }, - }, - ActionCache: data.ActionCache, // Propagate cache so container digest pins are applied - Features: data.Features, // Propagate features so cli-proxy image is included when enabled - } + detectionData := buildThreatDetectionWorkflowData(data, engineSetting) + detectionData.Tools = map[string]any{} images := collectDockerImages(detectionData.Tools, detectionData, c.actionMode) if len(images) == 0 { @@ -179,22 +197,11 @@ func (c *Compiler) buildInstallDetectionEngineForExternalDetectorStep(data *Work // Build a synthetic detection WorkflowData solely to generate the engine's // installation steps for this separate detection job context. - threatDetectionData := &WorkflowData{ - Tools: map[string]any{ - "bash": []any{"*"}, - }, - EngineConfig: &EngineConfig{ID: engineID}, - AI: engineID, - Features: data.Features, - Permissions: data.Permissions, - CachedPermissions: data.CachedPermissions, - IsDetectionRun: true, - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Type: SandboxTypeAWF, - }, - }, + threatDetectionData := buildThreatDetectionWorkflowData(data, engineID) + threatDetectionData.Tools = map[string]any{ + "bash": []any{"*"}, } + threatDetectionData.EngineConfig = &EngineConfig{ID: engineID} if canReuseThreatDetectionEngineConfigForExternalDetector(data, engineID) { ec := data.SafeOutputs.ThreatDetection.EngineConfig @@ -261,30 +268,19 @@ func (c *Compiler) buildExternalDetectorExecutionStep(data *WorkflowData) []stri // Build detection WorkflowData for the external detector. // The rw mount for ThreatDetectionDir allows the threat-detect binary to write // detection_result.json from inside the AWF container to the host filesystem. - threatDetectionData := &WorkflowData{ - Tools: map[string]any{ - "bash": []any{"*"}, - }, - EngineConfig: &EngineConfig{ID: engineID}, - AI: engineID, - Features: data.Features, - Permissions: data.Permissions, - CachedPermissions: data.CachedPermissions, - IsDetectionRun: true, - NetworkPermissions: &NetworkPermissions{ - Allowed: getThreatDetectionAdditionalAllowedDomains(data), - }, - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Type: SandboxTypeAWF, - // Add a read-write mount so the threat-detect binary can write - // detection_result.json inside the container and it becomes visible - // on the host through the bind mount. - Mounts: []string{ - constants.ThreatDetectionDir + ":" + constants.ThreatDetectionDir + ":rw", - }, - }, - }, + threatDetectionData := buildThreatDetectionWorkflowData(data, engineID) + threatDetectionData.Tools = map[string]any{ + "bash": []any{"*"}, + } + threatDetectionData.EngineConfig = &EngineConfig{ID: engineID} + threatDetectionData.NetworkPermissions = &NetworkPermissions{ + Allowed: getThreatDetectionAdditionalAllowedDomains(data), + } + threatDetectionData.SandboxConfig.Agent.Mounts = []string{ + // Add a read-write mount so the threat-detect binary can write + // detection_result.json inside the container and it becomes visible + // on the host through the bind mount. + constants.ThreatDetectionDir + ":" + constants.ThreatDetectionDir + ":rw", } // Inherit engine config overrides from threat-detection config when set. diff --git a/pkg/workflow/threat_detection_inline_engine.go b/pkg/workflow/threat_detection_inline_engine.go index dbf3d57f123..443417c058d 100644 --- a/pkg/workflow/threat_detection_inline_engine.go +++ b/pkg/workflow/threat_detection_inline_engine.go @@ -115,33 +115,17 @@ func (c *Compiler) buildDetectionEngineExecutionStep(data *WorkflowData) []strin // bash: ["*"] allows all shell commands — AWF's network firewall is the primary // constraint, so restricting individual bash commands inside the sandbox adds friction // without meaningful security benefit. - // RunnerConfig is propagated from the main workflow data so that arc-dind topology - // handling (daemon-visible Copilot staging step + daemon-visible spawn path) applies - // to the detection job the same way it applies to the agent job. // ModelMappings is propagated so the detection awf-config.json includes the alias map // (apiProxy.models). Without it, copilot_harness.cjs cannot resolve alias model names // (e.g. "small") to concrete ids before spawning the Copilot CLI in the detection job. - threatDetectionData := &WorkflowData{ - Tools: map[string]any{ - "bash": []any{"*"}, - }, - SafeOutputs: nil, - EngineConfig: detectionEngineConfig, - AI: engineSetting, - Features: data.Features, - Permissions: data.Permissions, - CachedPermissions: data.CachedPermissions, - IsDetectionRun: true, // Mark as detection run for phase tagging - RunnerConfig: data.RunnerConfig, // propagate runner.topology (e.g. arc-dind) to the detection job - ModelMappings: data.ModelMappings, // propagate alias map so detection awf-config.json can resolve model aliases - NetworkPermissions: &NetworkPermissions{ - Allowed: getThreatDetectionAdditionalAllowedDomains(data), - }, - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Type: SandboxTypeAWF, - }, - }, + threatDetectionData := buildThreatDetectionWorkflowData(data, engineSetting) + threatDetectionData.Tools = map[string]any{ + "bash": []any{"*"}, + } + threatDetectionData.EngineConfig = detectionEngineConfig + threatDetectionData.ModelMappings = data.ModelMappings // propagate alias map so detection awf-config.json can resolve model aliases + threatDetectionData.NetworkPermissions = &NetworkPermissions{ + Allowed: getThreatDetectionAdditionalAllowedDomains(data), } var steps []string diff --git a/pkg/workflow/threat_detection_test.go b/pkg/workflow/threat_detection_test.go index 7c8b4cb5cf9..2b810effbbe 100644 --- a/pkg/workflow/threat_detection_test.go +++ b/pkg/workflow/threat_detection_test.go @@ -1961,6 +1961,46 @@ func TestBuildPullAWFContainersStepPropagatesFeatures(t *testing.T) { }) } +func TestBuildPullAWFContainersStepPropagatesRunnerTopology(t *testing.T) { + compiler := NewCompiler() + buildToolsImagePrefix := constants.DefaultFirewallRegistry + "/build-tools:" + + t.Run("arc-dind includes build-tools image", func(t *testing.T) { + data := &WorkflowData{ + AI: "copilot", + RunnerConfig: &RunnerConfig{ + Topology: RunnerTopologyArcDind, + }, + SafeOutputs: &SafeOutputsConfig{ + ThreatDetection: &ThreatDetectionConfig{}, + }, + } + + steps := compiler.buildPullAWFContainersStep(data) + stepsString := strings.Join(steps, "") + + if !strings.Contains(stepsString, buildToolsImagePrefix) { + t.Errorf("expected build-tools image prefix %q in detection pull step for arc-dind;\ngot:\n%s", buildToolsImagePrefix, stepsString) + } + }) + + t.Run("non-arc-dind excludes build-tools image", func(t *testing.T) { + data := &WorkflowData{ + AI: "copilot", + SafeOutputs: &SafeOutputsConfig{ + ThreatDetection: &ThreatDetectionConfig{}, + }, + } + + steps := compiler.buildPullAWFContainersStep(data) + stepsString := strings.Join(steps, "") + + if strings.Contains(stepsString, buildToolsImagePrefix) { + t.Errorf("did not expect build-tools image prefix %q in detection pull step without arc-dind;\ngot:\n%s", buildToolsImagePrefix, stepsString) + } + }) +} + func TestBuildDetectionEngineExecutionStepEmitsNodeSetupForCopilot(t *testing.T) { compiler := NewCompiler() From f59b1216ee398c84cf07f28e3b88cdaef5138c04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:12:00 +0000 Subject: [PATCH 4/8] test: cover arc-dind external detector topology Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/threat_detection_external.go | 2 + pkg/workflow/threat_detection_test.go | 63 +++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/pkg/workflow/threat_detection_external.go b/pkg/workflow/threat_detection_external.go index 05d7a91d1d1..19b0fdbfb81 100644 --- a/pkg/workflow/threat_detection_external.go +++ b/pkg/workflow/threat_detection_external.go @@ -68,6 +68,8 @@ func codexProxyWebsocketBaseURL(apiBase string) string { // buildThreatDetectionWorkflowData creates the shared minimal WorkflowData used by // detection-job helper steps so topology- and feature-dependent behavior stays in sync. +// It always initializes SandboxConfig.Agent because downstream detection helpers +// extend the agent sandbox configuration (for example, external-detector mounts). func buildThreatDetectionWorkflowData(data *WorkflowData, engineID string) *WorkflowData { if engineID == "" { engineID = data.AI diff --git a/pkg/workflow/threat_detection_test.go b/pkg/workflow/threat_detection_test.go index 2b810effbbe..1f430716073 100644 --- a/pkg/workflow/threat_detection_test.go +++ b/pkg/workflow/threat_detection_test.go @@ -2001,6 +2001,69 @@ func TestBuildPullAWFContainersStepPropagatesRunnerTopology(t *testing.T) { }) } +func TestBuildExternalDetectorExecutionStepPropagatesRunnerTopology(t *testing.T) { + compiler := NewCompiler() + + t.Run("arc-dind uses daemon-visible AWF paths", func(t *testing.T) { + data := &WorkflowData{ + AI: "copilot", + RunnerConfig: &RunnerConfig{ + Topology: RunnerTopologyArcDind, + }, + SafeOutputs: &SafeOutputsConfig{ + ThreatDetection: &ThreatDetectionConfig{}, + }, + } + + steps := compiler.buildExternalDetectorExecutionStep(data) + if len(steps) == 0 { + t.Fatal("expected non-empty steps") + } + allSteps := strings.Join(steps, "") + + if !strings.Contains(allSteps, `--mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro"`) { + t.Errorf("expected arc-dind external detector execution to mount ${RUNNER_TEMP}/gh-aw read-only;\ngot:\n%s", allSteps) + } + if !strings.Contains(allSteps, `--mount "${RUNNER_TEMP}/gh-aw/home:${RUNNER_TEMP}/gh-aw/home:rw"`) { + t.Errorf("expected arc-dind external detector execution to mount ${RUNNER_TEMP}/gh-aw/home read-write;\ngot:\n%s", allSteps) + } + if !strings.Contains(allSteps, `\"proxyLogsDir\":\"${RUNNER_TEMP}/gh-aw/sandbox/firewall/logs\"`) { + t.Errorf("expected arc-dind external detector execution to rewrite proxyLogsDir under ${RUNNER_TEMP}/gh-aw;\ngot:\n%s", allSteps) + } + if !strings.Contains(allSteps, `\"auditDir\":\"${RUNNER_TEMP}/gh-aw/sandbox/firewall/audit\"`) { + t.Errorf("expected arc-dind external detector execution to rewrite auditDir under ${RUNNER_TEMP}/gh-aw;\ngot:\n%s", allSteps) + } + if !strings.Contains(allSteps, "export HOME=${RUNNER_TEMP}/gh-aw/home") { + t.Errorf("expected arc-dind external detector execution to export HOME under ${RUNNER_TEMP}/gh-aw/home;\ngot:\n%s", allSteps) + } + }) + + t.Run("non-arc-dind keeps standard AWF paths", func(t *testing.T) { + data := &WorkflowData{ + AI: "copilot", + SafeOutputs: &SafeOutputsConfig{ + ThreatDetection: &ThreatDetectionConfig{}, + }, + } + + steps := compiler.buildExternalDetectorExecutionStep(data) + if len(steps) == 0 { + t.Fatal("expected non-empty steps") + } + allSteps := strings.Join(steps, "") + + if strings.Contains(allSteps, `--mount "${RUNNER_TEMP}/gh-aw/home:${RUNNER_TEMP}/gh-aw/home:rw"`) { + t.Errorf("did not expect non-arc-dind external detector execution to mount ${RUNNER_TEMP}/gh-aw/home read-write;\ngot:\n%s", allSteps) + } + if strings.Contains(allSteps, "export HOME=${RUNNER_TEMP}/gh-aw/home") { + t.Errorf("did not expect non-arc-dind external detector execution to export HOME under ${RUNNER_TEMP}/gh-aw/home;\ngot:\n%s", allSteps) + } + if strings.Contains(allSteps, `\"proxyLogsDir\":\"${RUNNER_TEMP}/gh-aw/sandbox/firewall/logs\"`) { + t.Errorf("did not expect non-arc-dind external detector execution to rewrite proxyLogsDir under ${RUNNER_TEMP}/gh-aw;\ngot:\n%s", allSteps) + } + }) +} + func TestBuildDetectionEngineExecutionStepEmitsNodeSetupForCopilot(t *testing.T) { compiler := NewCompiler() From 3daa09f8e45162b394cb9bfc4195d797d93784b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:34:30 +0000 Subject: [PATCH 5/8] Address PR finisher follow-up Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../workflows/smoke-call-workflow.lock.yml | 21 +++++++++++++++--- pkg/workflow/threat_detection_external.go | 13 +++++------ pkg/workflow/threat_detection_test.go | 22 +++++++++++++++++++ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/.github/workflows/smoke-call-workflow.lock.yml b/.github/workflows/smoke-call-workflow.lock.yml index 8e6f0b9211f..17814deb639 100644 --- a/.github/workflows/smoke-call-workflow.lock.yml +++ b/.github/workflows/smoke-call-workflow.lock.yml @@ -619,6 +619,11 @@ jobs: "inputSchema": { "additionalProperties": false, "properties": { + "aw_context": { + "default": "", + "description": "Agent caller context (used internally by Agentic Workflows).", + "type": "string" + }, "payload": { "description": "Input parameter 'payload' for workflow smoke-workflow-call", "type": "string" @@ -1102,15 +1107,25 @@ jobs: needs: safe_outputs if: needs.safe_outputs.outputs.call_workflow_name == 'smoke-workflow-call' # Imported from called workflow "smoke-workflow-call" because GitHub requires the caller job to grant permissions requested by reusable workflow jobs. - # Review the called workflow's frontmatter permissions in ./.github/workflows/smoke-workflow-call.md. + # Review the called workflow's job-level permissions in ./.github/workflows/smoke-workflow-call.lock.yml. permissions: + actions: read contents: read - pull-requests: read + issues: write + pull-requests: write uses: ./.github/workflows/smoke-workflow-call.lock.yml with: + aw_context: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).aw_context }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} task-description: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload)['task-description'] }} - secrets: inherit + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_OTEL_GRAFANA_AUTHORIZATION: ${{ secrets.GH_AW_OTEL_GRAFANA_AUTHORIZATION }} + GH_AW_OTEL_GRAFANA_ENDPOINT: ${{ secrets.GH_AW_OTEL_GRAFANA_ENDPOINT }} + GH_AW_OTEL_SENTRY_AUTHORIZATION: ${{ secrets.GH_AW_OTEL_SENTRY_AUTHORIZATION }} + GH_AW_OTEL_SENTRY_ENDPOINT: ${{ secrets.GH_AW_OTEL_SENTRY_ENDPOINT }} conclusion: needs: diff --git a/pkg/workflow/threat_detection_external.go b/pkg/workflow/threat_detection_external.go index 19b0fdbfb81..530e7aeac50 100644 --- a/pkg/workflow/threat_detection_external.go +++ b/pkg/workflow/threat_detection_external.go @@ -102,11 +102,7 @@ func buildThreatDetectionWorkflowData(data *WorkflowData, engineID string) *Work func (c *Compiler) buildPullAWFContainersStep(data *WorkflowData) []string { // Build a minimal WorkflowData that represents the detection engine context so // collectDockerImages returns only the AWF firewall images (no MCP tool images). - engineSetting := data.AI - if engineSetting == "" { - engineSetting = "claude" - } - detectionData := buildThreatDetectionWorkflowData(data, engineSetting) + detectionData := buildThreatDetectionWorkflowData(data, "") detectionData.Tools = map[string]any{} images := collectDockerImages(detectionData.Tools, detectionData, c.actionMode) @@ -278,12 +274,13 @@ func (c *Compiler) buildExternalDetectorExecutionStep(data *WorkflowData) []stri threatDetectionData.NetworkPermissions = &NetworkPermissions{ Allowed: getThreatDetectionAdditionalAllowedDomains(data), } - threatDetectionData.SandboxConfig.Agent.Mounts = []string{ + threatDetectionData.SandboxConfig.Agent.Mounts = append( + threatDetectionData.SandboxConfig.Agent.Mounts, // Add a read-write mount so the threat-detect binary can write // detection_result.json inside the container and it becomes visible // on the host through the bind mount. - constants.ThreatDetectionDir + ":" + constants.ThreatDetectionDir + ":rw", - } + constants.ThreatDetectionDir+":"+constants.ThreatDetectionDir+":rw", + ) // Inherit engine config overrides from threat-detection config when set. if canReuseThreatDetectionEngineConfigForExternalDetector(data, engineID) { diff --git a/pkg/workflow/threat_detection_test.go b/pkg/workflow/threat_detection_test.go index 1f430716073..da65ed35c31 100644 --- a/pkg/workflow/threat_detection_test.go +++ b/pkg/workflow/threat_detection_test.go @@ -1999,6 +1999,28 @@ func TestBuildPullAWFContainersStepPropagatesRunnerTopology(t *testing.T) { t.Errorf("did not expect build-tools image prefix %q in detection pull step without arc-dind;\ngot:\n%s", buildToolsImagePrefix, stepsString) } }) + + t.Run("permissions do not change pulled images", func(t *testing.T) { + baseData := &WorkflowData{ + AI: "copilot", + SafeOutputs: &SafeOutputsConfig{ + ThreatDetection: &ThreatDetectionConfig{}, + }, + } + withPermissions := &WorkflowData{ + AI: baseData.AI, + SafeOutputs: baseData.SafeOutputs, + Permissions: "contents: read", + CachedPermissions: NewPermissionsContentsRead(), + } + + baseSteps := strings.Join(compiler.buildPullAWFContainersStep(baseData), "") + permissionSteps := strings.Join(compiler.buildPullAWFContainersStep(withPermissions), "") + + if permissionSteps != baseSteps { + t.Errorf("expected detection pull step to ignore permissions when collecting images;\nwithout permissions:\n%s\nwith permissions:\n%s", baseSteps, permissionSteps) + } + }) } func TestBuildExternalDetectorExecutionStepPropagatesRunnerTopology(t *testing.T) { From 810ff1cea9f2b3d15f124d4414737c1d4b0ac6eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:38:48 +0000 Subject: [PATCH 6/8] Fix validation whitespace issue Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- docs/tests/mobile-responsive.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/tests/mobile-responsive.spec.ts b/docs/tests/mobile-responsive.spec.ts index 528648e1d92..5e553403d57 100644 --- a/docs/tests/mobile-responsive.spec.ts +++ b/docs/tests/mobile-responsive.spec.ts @@ -222,7 +222,6 @@ test.describe('Mobile and Responsive Layout', () => { await context.close(); }); - // Regression test for https://github.com/github/gh-aw/issues/29545 // Verify the navigation dropdown is fully within the viewport when large // user fonts cause header elements to shift on Android Chrome. From 63eb49c721d7eb7e3c9ff06d30d54c97f6857566 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:43:37 +0000 Subject: [PATCH 7/8] Clarify detection helper defaulting Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/threat_detection_external.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/workflow/threat_detection_external.go b/pkg/workflow/threat_detection_external.go index 530e7aeac50..47c73903362 100644 --- a/pkg/workflow/threat_detection_external.go +++ b/pkg/workflow/threat_detection_external.go @@ -70,6 +70,8 @@ func codexProxyWebsocketBaseURL(apiBase string) string { // detection-job helper steps so topology- and feature-dependent behavior stays in sync. // It always initializes SandboxConfig.Agent because downstream detection helpers // extend the agent sandbox configuration (for example, external-detector mounts). +// Callers can pass an empty engineID to inherit the detection job's default engine +// resolution from the source WorkflowData. func buildThreatDetectionWorkflowData(data *WorkflowData, engineID string) *WorkflowData { if engineID == "" { engineID = data.AI From 60526df44d21795ef0b758b23e508ecbd9e99901 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:52:40 +0000 Subject: [PATCH 8/8] Test threat detection mount append Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/threat_detection_external.go | 20 +++++++++----- pkg/workflow/threat_detection_test.go | 32 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/pkg/workflow/threat_detection_external.go b/pkg/workflow/threat_detection_external.go index 47c73903362..5ce1301a706 100644 --- a/pkg/workflow/threat_detection_external.go +++ b/pkg/workflow/threat_detection_external.go @@ -3,6 +3,7 @@ package workflow import ( "fmt" + "slices" "strconv" "strings" @@ -246,6 +247,14 @@ func isAWFBinaryInstallStep(step GitHubActionStep) bool { return false } +func appendThreatDetectionRWMount(mounts []string) []string { + threatDetectionMount := constants.ThreatDetectionDir + ":" + constants.ThreatDetectionDir + ":rw" + if slices.Contains(mounts, threatDetectionMount) { + return mounts + } + return append(mounts, threatDetectionMount) +} + // buildExternalDetectorExecutionStep creates the AWF execution step for the external // threat-detect binary. It runs threat-detect inside the AWF firewall sandbox with a // read-write mount so detection_result.json can be written from inside the container @@ -276,13 +285,10 @@ func (c *Compiler) buildExternalDetectorExecutionStep(data *WorkflowData) []stri threatDetectionData.NetworkPermissions = &NetworkPermissions{ Allowed: getThreatDetectionAdditionalAllowedDomains(data), } - threatDetectionData.SandboxConfig.Agent.Mounts = append( - threatDetectionData.SandboxConfig.Agent.Mounts, - // Add a read-write mount so the threat-detect binary can write - // detection_result.json inside the container and it becomes visible - // on the host through the bind mount. - constants.ThreatDetectionDir+":"+constants.ThreatDetectionDir+":rw", - ) + // Add a read-write mount so the threat-detect binary can write + // detection_result.json inside the container and it becomes visible + // on the host through the bind mount. + threatDetectionData.SandboxConfig.Agent.Mounts = appendThreatDetectionRWMount(threatDetectionData.SandboxConfig.Agent.Mounts) // Inherit engine config overrides from threat-detection config when set. if canReuseThreatDetectionEngineConfigForExternalDetector(data, engineID) { diff --git a/pkg/workflow/threat_detection_test.go b/pkg/workflow/threat_detection_test.go index da65ed35c31..d4a0410c671 100644 --- a/pkg/workflow/threat_detection_test.go +++ b/pkg/workflow/threat_detection_test.go @@ -3,6 +3,7 @@ package workflow import ( + "reflect" "strings" "testing" @@ -2086,6 +2087,37 @@ func TestBuildExternalDetectorExecutionStepPropagatesRunnerTopology(t *testing.T }) } +func TestAppendThreatDetectionRWMount(t *testing.T) { + threatDetectionMount := constants.ThreatDetectionDir + ":" + constants.ThreatDetectionDir + ":rw" + + t.Run("appends missing mount without clobbering existing mounts", func(t *testing.T) { + existingMounts := []string{ + "/tmp/existing:/tmp/existing:ro", + "/tmp/other:/tmp/other:rw", + } + + got := appendThreatDetectionRWMount(append([]string(nil), existingMounts...)) + + want := append(append([]string(nil), existingMounts...), threatDetectionMount) + if !reflect.DeepEqual(got, want) { + t.Fatalf("expected mounts %v, got %v", want, got) + } + }) + + t.Run("does not duplicate existing threat-detection mount", func(t *testing.T) { + existingMounts := []string{ + "/tmp/existing:/tmp/existing:ro", + threatDetectionMount, + } + + got := appendThreatDetectionRWMount(append([]string(nil), existingMounts...)) + + if !reflect.DeepEqual(got, existingMounts) { + t.Fatalf("expected mounts %v, got %v", existingMounts, got) + } + }) +} + func TestBuildDetectionEngineExecutionStepEmitsNodeSetupForCopilot(t *testing.T) { compiler := NewCompiler()