Summary
When a workflow uses the Copilot engine in BYOK mode (COPILOT_PROVIDER_BASE_URL set in engine.env) and adds the BYOK provider host to network.allowed, the domain is added to the firewall allow-list of the main agent job, but it is not propagated to the separate threat-detection job's Execute GitHub Copilot CLI step.
The threat-detection job still inherits engine.env (including COPILOT_PROVIDER_BASE_URL), so the detection Copilot CLI runs in BYOK mode and tries to reach the third-party provider — but the AWF firewall for that step is built from a minimal, hard-coded domain set with an empty network.allowed, so the request to the provider host is blocked and the step fails.
Net effect: a workflow that runs fine end-to-end with the GitHub-hosted Copilot backend fails in the detection job as soon as you switch to BYOK, even though network.allowed correctly lists the provider host.
Is this intended or a bug?
It is partly intended, partly a bug:
- Intended: the detection job deliberately runs with a minimal network (security isolation). The detection engine is built with
NetworkPermissions.Allowed: []string{} and uses the threat-detection ecosystem domain list (GitHub first-party Copilot endpoints only). For the default GitHub-hosted Copilot backend this is correct and sufficient — the detection CLI only needs api.githubcopilot.com et al., which are in that list.
- Bug / gap: this design does not account for BYOK. In BYOK mode the detection CLI must reach the user-configured provider host, but (a) the user's
network.allowed entries are intentionally stripped for detection, and (b) the BYOK base URL (COPILOT_PROVIDER_BASE_URL) is not one of the env vars that gets auto-added to the firewall allow-list (only engine.api-target / GITHUB_COPILOT_BASE_URL are). So the provider host is reachable nowhere in the detection job, and the step fails.
So the minimal-network policy is intended, but the lack of any path to allow the BYOK provider host in the detection job is a bug.
Analysis (source code)
1. The detection engine is built with an empty network.allowed
buildDetectionEngineExecutionStep constructs a minimal WorkflowData for the detection run. It explicitly empties NetworkPermissions.Allowed and marks the run as a detection run, but it does inherit engine.env (which carries COPILOT_PROVIDER_BASE_URL for BYOK):
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
NetworkPermissions: &NetworkPermissions{
Allowed: []string{}, // no user-specified additional domains; engine provides its own minimal set
},
SandboxConfig: &SandboxConfig{
Agent: &AgentSandboxConfig{
Type: SandboxTypeAWF,
},
},
}
detectionEngineConfig inherits Env from the main engine config, so COPILOT_PROVIDER_BASE_URL is forwarded to the detection step — the detection CLI therefore runs in BYOK mode:
detectionEngineConfig = &EngineConfig{
ID: detectionEngineConfig.ID,
Model: detectionEngineConfig.Model,
Version: detectionEngineConfig.Version,
Env: detectionEngineConfig.Env,
Config: detectionEngineConfig.Config,
Args: detectionEngineConfig.Args,
APITarget: detectionEngineConfig.APITarget,
HarnessScript: detectionEngineConfig.HarnessScript,
CopilotSDKDriver: detectionEngineConfig.CopilotSDKDriver,
}
2. For detection runs, the Copilot engine uses the minimal domain set + only the api-target merge
In CopilotEngine.GetExecutionSteps, the allow-list for a detection run comes from GetThreatDetectionAllowedDomains(workflowData.NetworkPermissions) — and because NetworkPermissions.Allowed is empty (see above), nothing user-specified is merged. The only extra merge is GetCopilotAPITarget, which does not read the BYOK var:
var allowedDomains string
if workflowData.IsDetectionRun {
allowedDomains = GetThreatDetectionAllowedDomains(workflowData.NetworkPermissions)
} else if workflowData.CachedAllowedDomainsComputed {
// Use the pre-warmed cache (populated before GetExecutionSteps is called)
// to avoid re-running the expensive map+sort operation.
allowedDomains = workflowData.CachedAllowedDomainsStr
} else {
allowedDomains = GetAllowedDomainsForEngine(constants.CopilotEngine, workflowData.NetworkPermissions, workflowData.Tools, workflowData.Runtimes)
}
// Add Copilot API target domains to the firewall allow-list.
// Resolved from engine.api-target or GITHUB_COPILOT_BASE_URL in engine.env.
if copilotAPITarget := GetCopilotAPITarget(workflowData); copilotAPITarget != "" {
allowedDomains = mergeAPITargetDomains(allowedDomains, copilotAPITarget)
}
BYOK mode is detected via COPILOT_PROVIDER_BASE_URL, a different env var from the one the api-target merge understands:
// isBYOKMode is true when the user has set COPILOT_PROVIDER_BASE_URL in engine.env,
// which routes Copilot requests to a non-GitHub provider. In that mode the GitHub
// identity token (COPILOT_GITHUB_TOKEN) must NOT be injected into the step env:
// forwarding it to a third-party host would be a credential leak.
isBYOKMode := engineEnvHasKey(workflowData, constants.CopilotProviderBaseURL)
3. GetThreatDetectionAllowedDomains only adds first-party Copilot domains
It loads the threat-detection ecosystem and merges network — but network is the empty one from step 1, so the result is just the GitHub first-party set:
func GetThreatDetectionAllowedDomains(network *NetworkPermissions) string {
detectionDomains := getEcosystemDomains("threat-detection")
// Pass nil tools and runtimes: detection runs with no npm/runtime ecosystem, so
// ecosystem domain expansion is intentionally skipped.
return mergeDomainsWithNetworkToolsAndRuntimes(detectionDomains, network, nil, nil)
}
The threat-detection ecosystem contains no third-party provider hosts:
"threat-detection": [
"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",
4. The api-target merge can never rescue BYOK
GetCopilotAPITarget only resolves engine.api-target or GITHUB_COPILOT_BASE_URL — never COPILOT_PROVIDER_BASE_URL:
func GetCopilotAPITarget(workflowData *WorkflowData) string {
awfHelpersLog.Print("Getting Copilot API target")
// Explicit engine.api-target takes precedence.
if workflowData != nil && workflowData.EngineConfig != nil && workflowData.EngineConfig.APITarget != "" {
awfHelpersLog.Printf("Using explicit Copilot api-target: %s", workflowData.EngineConfig.APITarget)
return workflowData.EngineConfig.APITarget
}
// Fallback: derive from the well-known GITHUB_COPILOT_BASE_URL env var.
awfHelpersLog.Print("No explicit api-target, deriving Copilot API target from GITHUB_COPILOT_BASE_URL")
return extractAPITargetHost(workflowData, "GITHUB_COPILOT_BASE_URL")
}
Conclusion
There is no code path that lets the threat-detection Copilot CLI step reach a BYOK provider host:
- user
network.allowed is intentionally emptied for detection (step 1),
- the detection ecosystem list contains only GitHub first-party hosts (step 3),
- and the only auto-merge available (
api-target) ignores the BYOK var COPILOT_PROVIDER_BASE_URL (steps 2 & 4).
Meanwhile the detection CLI does run in BYOK mode (because engine.env is inherited), so it sends requests to the provider host, which the AWF firewall blocks → the Execute GitHub Copilot CLI step fails in the detection job.
Reproduction
Minimal workflow (Copilot engine, BYOK, provider host in network.allowed, threat-detection on by default because safe-outputs are configured):
---
on: issues
engine:
id: copilot
env:
COPILOT_PROVIDER_BASE_URL: ${{ secrets.PROVIDER_BASE_URL }} # e.g. https://llm.corp.example.com/v1
COPILOT_PROVIDER_API_KEY: ${{ secrets.PROVIDER_API_KEY }}
network:
allowed:
- defaults
- llm.corp.example.com # BYOK provider host
safe-outputs:
add-comment: {}
---
Triage this issue.
gh aw compile the workflow and inspect the generated .lock.yml.
- In the agent job's
Execute GitHub Copilot CLI step, the AWF --allow-domains (and GH_AW_ALLOWED_DOMAINS) include llm.corp.example.com. ✅
- In the detection job's
Execute GitHub Copilot CLI step, the AWF --allow-domains contain only the threat-detection ecosystem hosts — llm.corp.example.com is absent. ❌
- At runtime, the detection step's Copilot CLI (running in BYOK mode via the inherited
COPILOT_PROVIDER_BASE_URL) attempts to reach llm.corp.example.com and is blocked by the firewall; the detection job fails (or warns, depending on continue-on-error) with a network/connection error to the provider host.
Expected behavior
When the Copilot engine runs in BYOK mode and the threat-detection job actually invokes the Copilot CLI, the detection job's firewall allow-list should include the BYOK provider host so the detection run can authenticate and call the model — without broadly re-opening the network for detection.
Concretely, at least one of the following should hold:
- The BYOK provider host (derived from
COPILOT_PROVIDER_BASE_URL, mirroring how GITHUB_COPILOT_BASE_URL is handled) is automatically added to the detection allow-list, and/or
- The detection allow-list includes the user's
network.allowed entries needed for the engine to function in BYOK mode.
The default (non-BYOK) detection behavior — minimal first-party-only network — should remain unchanged.
Proposed fix (options)
These are alternatives; option A is the most targeted and preserves the minimal-network intent.
- Option A — Always merge the Copilot API target for BYOK in detection (recommended). Make
GetCopilotAPITarget (or the detection-specific allow-list construction) also resolve COPILOT_PROVIDER_BASE_URL, and ensure the resulting host is merged into the detection allow-list. Because engine.env is already inherited by the detection config, the host is derivable at compile time. This adds only the provider host, keeping detection otherwise minimal.
- Option B — Propagate the BYOK provider host from
network.allowed into the detection config. When building threatDetectionData, instead of Allowed: []string{}, seed it with just the BYOK-relevant host(s) the engine needs (e.g. the host parsed from COPILOT_PROVIDER_BASE_URL), still excluding the rest of the user's broad network.allowed.
- Option C — Add a small opt-in. Provide
safe-outputs.threat-detection.network (or similar) so BYOK users can explicitly grant the provider host to the detection job. More flexible but adds surface area; A/B fix the common case automatically.
Implementation plan
A core team member / agent can implement Option A as follows (reuse the existing mergeAPITargetDomains / GetAPITargetDomains helpers so behavior matches GITHUB_COPILOT_BASE_URL):
-
Resolve the BYOK provider host (pkg/workflow/engine_api_targets.go):
- Add a helper (e.g.
GetCopilotProviderTarget(workflowData) or extend GetCopilotAPITarget) that resolves the host from COPILOT_PROVIDER_BASE_URL in engine.env via the existing extractAPITargetHost(workflowData, "COPILOT_PROVIDER_BASE_URL") path. Decide precedence vs. GITHUB_COPILOT_BASE_URL/engine.api-target (BYOK base URL should win when set, since it's where requests actually go).
-
Merge it into the detection allow-list (pkg/workflow/copilot_engine_execution.go, ~lines 369–383):
- After computing
allowedDomains for the IsDetectionRun branch, merge the resolved BYOK provider host using mergeAPITargetDomains(allowedDomains, providerHost) (mirroring the existing copilotAPITarget merge). Ensure this also applies to the non-detection branch if not already covered, so the agent and detection jobs stay in sync for BYOK.
-
Keep GH_AW_ALLOWED_DOMAINS in sync (pkg/workflow/domains.go, computeAllowedDomainsForSanitization, ~lines 972–988):
- Add the BYOK provider target alongside the existing
GetCopilotAPITarget / Antigravity / Gemini merges so the sanitization allow-list and the firewall allow-list agree.
-
Tests:
pkg/workflow/copilot_engine_test.go (or a focused new test) — compile a BYOK workflow with COPILOT_PROVIDER_BASE_URL set and assert the provider host appears in the detection job's --allow-domains, not just the agent job's.
- Extend
pkg/workflow/awf_helpers_test.go (around the existing GITHUB_COPILOT_BASE_URL api-target cases) with COPILOT_PROVIDER_BASE_URL cases.
- Add a regression test asserting that with no BYOK var set, the detection allow-list is unchanged (only the
threat-detection ecosystem).
-
Documentation:
- Update the BYOK / Copilot engine docs and the network/firewall reference to note that the BYOK provider host is automatically allowed in both the agent and threat-detection jobs (and how to configure it).
-
Follow project guidelines:
- Run
make fmt after Go changes and make recompile after any workflow markdown changes.
- Run
make agent-finish (build, test, recompile, lint, lint-errors) before completing.
- Error messages (if any added) follow [what's wrong]. [what's expected]. [example].
Notes / open questions
- Precedence: when both
COPILOT_PROVIDER_BASE_URL (BYOK) and GITHUB_COPILOT_BASE_URL/engine.api-target are set, which host(s) should be added to the detection allow-list? (Likely the BYOK host, since that's where the detection CLI's requests go; possibly both.)
- Detection in BYOK is intended to call the model — confirm the team wants threat-detection to actually run through the BYOK provider. If detection should instead always use the GitHub-hosted backend regardless of the agent's BYOK setting, the fix would flip the other way: stop inheriting
COPILOT_PROVIDER_BASE_URL into the detection config (and the related provider key/token) rather than opening the firewall.
- This is a non-breaking change: default (non-BYOK) detection behavior is unchanged; it only adds the provider host when BYOK is configured.
- Other BYOK-capable engines (OpenCode, Crush, Pi) already derive provider domains from the model prefix via
GetDefaultDomainsForEngine; this issue is specific to the Copilot engine's BYOK-via-COPILOT_PROVIDER_BASE_URL path interacting with the detection job's empty network.allowed.
Summary
When a workflow uses the Copilot engine in BYOK mode (
COPILOT_PROVIDER_BASE_URLset inengine.env) and adds the BYOK provider host tonetwork.allowed, the domain is added to the firewall allow-list of the main agent job, but it is not propagated to the separate threat-detection job'sExecute GitHub Copilot CLIstep.The threat-detection job still inherits
engine.env(includingCOPILOT_PROVIDER_BASE_URL), so the detection Copilot CLI runs in BYOK mode and tries to reach the third-party provider — but the AWF firewall for that step is built from a minimal, hard-coded domain set with an emptynetwork.allowed, so the request to the provider host is blocked and the step fails.Net effect: a workflow that runs fine end-to-end with the GitHub-hosted Copilot backend fails in the detection job as soon as you switch to BYOK, even though
network.allowedcorrectly lists the provider host.Is this intended or a bug?
It is partly intended, partly a bug:
NetworkPermissions.Allowed: []string{}and uses thethreat-detectionecosystem domain list (GitHub first-party Copilot endpoints only). For the default GitHub-hosted Copilot backend this is correct and sufficient — the detection CLI only needsapi.githubcopilot.comet al., which are in that list.network.allowedentries are intentionally stripped for detection, and (b) the BYOK base URL (COPILOT_PROVIDER_BASE_URL) is not one of the env vars that gets auto-added to the firewall allow-list (onlyengine.api-target/GITHUB_COPILOT_BASE_URLare). So the provider host is reachable nowhere in the detection job, and the step fails.So the minimal-network policy is intended, but the lack of any path to allow the BYOK provider host in the detection job is a bug.
Analysis (source code)
1. The detection engine is built with an empty
network.allowedbuildDetectionEngineExecutionStepconstructs a minimalWorkflowDatafor the detection run. It explicitly emptiesNetworkPermissions.Allowedand marks the run as a detection run, but it does inheritengine.env(which carriesCOPILOT_PROVIDER_BASE_URLfor BYOK):detectionEngineConfiginheritsEnvfrom the main engine config, soCOPILOT_PROVIDER_BASE_URLis forwarded to the detection step — the detection CLI therefore runs in BYOK mode:2. For detection runs, the Copilot engine uses the minimal domain set + only the
api-targetmergeIn
CopilotEngine.GetExecutionSteps, the allow-list for a detection run comes fromGetThreatDetectionAllowedDomains(workflowData.NetworkPermissions)— and becauseNetworkPermissions.Allowedis empty (see above), nothing user-specified is merged. The only extra merge isGetCopilotAPITarget, which does not read the BYOK var:BYOK mode is detected via
COPILOT_PROVIDER_BASE_URL, a different env var from the one the api-target merge understands:3.
GetThreatDetectionAllowedDomainsonly adds first-party Copilot domainsIt loads the
threat-detectionecosystem and mergesnetwork— butnetworkis the empty one from step 1, so the result is just the GitHub first-party set:The
threat-detectionecosystem contains no third-party provider hosts:4. The
api-targetmerge can never rescue BYOKGetCopilotAPITargetonly resolvesengine.api-targetorGITHUB_COPILOT_BASE_URL— neverCOPILOT_PROVIDER_BASE_URL:Conclusion
There is no code path that lets the threat-detection Copilot CLI step reach a BYOK provider host:
network.allowedis intentionally emptied for detection (step 1),api-target) ignores the BYOK varCOPILOT_PROVIDER_BASE_URL(steps 2 & 4).Meanwhile the detection CLI does run in BYOK mode (because
engine.envis inherited), so it sends requests to the provider host, which the AWF firewall blocks → theExecute GitHub Copilot CLIstep fails in the detection job.Reproduction
Minimal workflow (Copilot engine, BYOK, provider host in
network.allowed, threat-detection on by default because safe-outputs are configured):--- on: issues engine: id: copilot env: COPILOT_PROVIDER_BASE_URL: ${{ secrets.PROVIDER_BASE_URL }} # e.g. https://llm.corp.example.com/v1 COPILOT_PROVIDER_API_KEY: ${{ secrets.PROVIDER_API_KEY }} network: allowed: - defaults - llm.corp.example.com # BYOK provider host safe-outputs: add-comment: {} --- Triage this issue.gh aw compilethe workflow and inspect the generated.lock.yml.Execute GitHub Copilot CLIstep, the AWF--allow-domains(andGH_AW_ALLOWED_DOMAINS) includellm.corp.example.com. ✅Execute GitHub Copilot CLIstep, the AWF--allow-domainscontain only thethreat-detectionecosystem hosts —llm.corp.example.comis absent. ❌COPILOT_PROVIDER_BASE_URL) attempts to reachllm.corp.example.comand is blocked by the firewall; the detection job fails (or warns, depending oncontinue-on-error) with a network/connection error to the provider host.Expected behavior
When the Copilot engine runs in BYOK mode and the threat-detection job actually invokes the Copilot CLI, the detection job's firewall allow-list should include the BYOK provider host so the detection run can authenticate and call the model — without broadly re-opening the network for detection.
Concretely, at least one of the following should hold:
COPILOT_PROVIDER_BASE_URL, mirroring howGITHUB_COPILOT_BASE_URLis handled) is automatically added to the detection allow-list, and/ornetwork.allowedentries needed for the engine to function in BYOK mode.The default (non-BYOK) detection behavior — minimal first-party-only network — should remain unchanged.
Proposed fix (options)
These are alternatives; option A is the most targeted and preserves the minimal-network intent.
GetCopilotAPITarget(or the detection-specific allow-list construction) also resolveCOPILOT_PROVIDER_BASE_URL, and ensure the resulting host is merged into the detection allow-list. Becauseengine.envis already inherited by the detection config, the host is derivable at compile time. This adds only the provider host, keeping detection otherwise minimal.network.allowedinto the detection config. When buildingthreatDetectionData, instead ofAllowed: []string{}, seed it with just the BYOK-relevant host(s) the engine needs (e.g. the host parsed fromCOPILOT_PROVIDER_BASE_URL), still excluding the rest of the user's broadnetwork.allowed.safe-outputs.threat-detection.network(or similar) so BYOK users can explicitly grant the provider host to the detection job. More flexible but adds surface area; A/B fix the common case automatically.Implementation plan
A core team member / agent can implement Option A as follows (reuse the existing
mergeAPITargetDomains/GetAPITargetDomainshelpers so behavior matchesGITHUB_COPILOT_BASE_URL):Resolve the BYOK provider host (
pkg/workflow/engine_api_targets.go):GetCopilotProviderTarget(workflowData)or extendGetCopilotAPITarget) that resolves the host fromCOPILOT_PROVIDER_BASE_URLinengine.envvia the existingextractAPITargetHost(workflowData, "COPILOT_PROVIDER_BASE_URL")path. Decide precedence vs.GITHUB_COPILOT_BASE_URL/engine.api-target(BYOK base URL should win when set, since it's where requests actually go).Merge it into the detection allow-list (
pkg/workflow/copilot_engine_execution.go, ~lines 369–383):allowedDomainsfor theIsDetectionRunbranch, merge the resolved BYOK provider host usingmergeAPITargetDomains(allowedDomains, providerHost)(mirroring the existingcopilotAPITargetmerge). Ensure this also applies to the non-detection branch if not already covered, so the agent and detection jobs stay in sync for BYOK.Keep
GH_AW_ALLOWED_DOMAINSin sync (pkg/workflow/domains.go,computeAllowedDomainsForSanitization, ~lines 972–988):GetCopilotAPITarget/ Antigravity / Gemini merges so the sanitization allow-list and the firewall allow-list agree.Tests:
pkg/workflow/copilot_engine_test.go(or a focused new test) — compile a BYOK workflow withCOPILOT_PROVIDER_BASE_URLset and assert the provider host appears in the detection job's--allow-domains, not just the agent job's.pkg/workflow/awf_helpers_test.go(around the existingGITHUB_COPILOT_BASE_URLapi-target cases) withCOPILOT_PROVIDER_BASE_URLcases.threat-detectionecosystem).Documentation:
Follow project guidelines:
make fmtafter Go changes andmake recompileafter any workflow markdown changes.make agent-finish(build, test, recompile, lint, lint-errors) before completing.Notes / open questions
COPILOT_PROVIDER_BASE_URL(BYOK) andGITHUB_COPILOT_BASE_URL/engine.api-targetare set, which host(s) should be added to the detection allow-list? (Likely the BYOK host, since that's where the detection CLI's requests go; possibly both.)COPILOT_PROVIDER_BASE_URLinto the detection config (and the related provider key/token) rather than opening the firewall.GetDefaultDomainsForEngine; this issue is specific to the Copilot engine's BYOK-via-COPILOT_PROVIDER_BASE_URLpath interacting with the detection job's emptynetwork.allowed.