Skip to content

network.allowed domains are not propagated to the threat-detection job's "Execute GitHub Copilot CLI" step, breaking BYOK (Copilot engine) #39206

@shubhamtanwar23

Description

@shubhamtanwar23

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.
  1. gh aw compile the workflow and inspect the generated .lock.yml.
  2. In the agent job's Execute GitHub Copilot CLI step, the AWF --allow-domains (and GH_AW_ALLOWED_DOMAINS) include llm.corp.example.com. ✅
  3. 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. ❌
  4. 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):

  1. 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).
  2. 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.
  3. 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.
  4. 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).
  5. 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).
  6. 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.

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions