Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/aw/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ Keywords expanding to curated domain lists:
| `python-native` | Python native build deps | Native toolchain mirrors for building Python packages from source |
| `copilot-vendor` | Copilot plan-specific APIs / telemetry | `api.business.githubcopilot.com`, `api.enterprise.githubcopilot.com`, `api.individual.githubcopilot.com`, `telemetry.enterprise.githubcopilot.com` |

## Automatic Engine Domain Sets

Each engine automatically receives the domain set it requires in addition to
`network.allowed`. These named sets are maintained by the compiler for analysis
and reporting; they are not valid `network.allowed` identifiers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 03a3bcb: updated .github/aw/network.md with the same Copilot-only automatic threat-detection scoping and clarified other detection engines use their own defaults.


| Engine set | Included domains |
|---|---|
| `copilot` | `api.github.com`, `api.githubcopilot.com`, `github.com`, `host.docker.internal`, `raw.githubusercontent.com` |
| `claude` | Anthropic APIs, GitHub transport, certificate/OCSP services, Ubuntu package metadata, Playwright downloads, and `host.docker.internal` |
| `codex` | `172.30.0.1`, `api.github.com`, `api.openai.com`, `chatgpt.com`, `github.com`, `host.docker.internal`, `openai.com` |
| `gemini` | `*.googleapis.com`, `generativelanguage.googleapis.com`, `github.com`, `host.docker.internal`, `raw.githubusercontent.com` |
| `pi` | `api.githubcopilot.com`, `github.com`, `host.docker.internal`, `raw.githubusercontent.com`; provider-scoped models replace the API host with the selected provider endpoint |
| `threat-detection` | Copilot API and telemetry hosts, `api.github.com`, `github.com`, `host.docker.internal`, and `registry.npmjs.org` for read-only lockfile validation |

## Invalid Shorthands

These look like ecosystem identifiers but are **not recognised** — using them causes a **compile-time error**:
Expand Down
15 changes: 15 additions & 0 deletions docs/src/content/docs/reference/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ Mix ecosystem identifiers with specific domains for fine-grained control:
| `swift` | Swift packages (`swift.org`, `cocoapods.org`) |
| `zig` | Zig packages (`ziglang.org`) |

## Automatic Engine Domain Sets

Each engine automatically receives the domain set it requires in addition to
`network.allowed`. These named sets are maintained by the compiler for analysis
and reporting; they are not valid `network.allowed` identifiers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 03a3bcb: scoped the docs to say the threat-detection set is applied automatically only to Copilot threat-detection runs; external Claude, Codex, Gemini, and other detection runs use their own engine defaults.


| Engine set | Included domains |
|---|---|
| `copilot` | `api.github.com`, `api.githubcopilot.com`, `github.com`, `host.docker.internal`, `raw.githubusercontent.com` |
| `claude` | Anthropic APIs, GitHub transport, certificate/OCSP services, Ubuntu package metadata, Playwright downloads, and `host.docker.internal` |
| `codex` | `172.30.0.1`, `api.github.com`, `api.openai.com`, `chatgpt.com`, `github.com`, `host.docker.internal`, `openai.com` |
| `gemini` | `*.googleapis.com`, `generativelanguage.googleapis.com`, `github.com`, `host.docker.internal`, `raw.githubusercontent.com` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The pi-base set exists in engineDefaultDomainSets and is exported via GetEngineDefaultDomainSets() / PiBaseDefaultDomains, but it is absent from both doc tables — leaving a gap between the code and the reference docs.

💡 Suggested addition

Add a row after the pi row in both docs/src/content/docs/reference/network.md and .github/aw/network.md:

| `pi-base` | `host.docker.internal`, `github.com`, `raw.githubusercontent.com`; applied as the provider-independent baseline before a provider prefix is resolved |

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 03a3bcb: added the missing pi-base row to both network reference tables.

| `pi` | `api.githubcopilot.com`, `github.com`, `host.docker.internal`, `raw.githubusercontent.com`; provider-scoped models replace the API host with the selected provider endpoint |
| `threat-detection` | Copilot API and telemetry hosts, `api.github.com`, `github.com`, `host.docker.internal`, and `registry.npmjs.org` for read-only lockfile validation |

### Ecosystem Identifier Validation

Single-word entries in `network.allowed` that match the ecosystem identifier pattern (`[a-z][a-z0-9-]*`) are validated against the known ecosystem list at compile time. An unrecognized identifier produces a compilation error with the full list of valid options:
Expand Down
11 changes: 0 additions & 11 deletions pkg/workflow/data/ecosystem_domains.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,5 @@
"scala": ["repo.scala-sbt.org", "scala-ci.typesafe.com", "repo.typesafe.com", "jitpack.io", "dl.bintray.com", "scala.jfrog.io"],
"swift": ["download.swift.org", "swift.org", "cocoapods.org", "cdn.cocoapods.org"],
"terraform": ["releases.hashicorp.com", "apt.releases.hashicorp.com", "yum.releases.hashicorp.com", "registry.terraform.io"],
"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",
"telemetry.enterprise.githubcopilot.com"
],
"zig": ["ziglang.org", "pkg.machengine.org", "deps.files.ghostty.org"]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 03a3bcb: restored the legacy threat-detection ecosystem alias in the embedded JSON so network.allowed: [threat-detection] remains accepted and expands as before.

}
225 changes: 133 additions & 92 deletions pkg/workflow/domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,104 +65,150 @@ func getLoadedEcosystemDomains() map[string][]string {
// to an engine default and that test starts failing, the domain belongs behind an explicit
// ecosystem/runtime opt-in instead, not in the unconditional default list.

// engineDefaultDomainSets centralizes every unconditional engine allow-list. These
// sets are intentionally distinct from user-selectable ecosystem domain lists: the
// compiler includes them automatically for the matching engine, rather than allowing
// users to select them through network.allowed.
//
// Engine default domain lists intentionally exclude package registries (npm, PyPI,
// and similar). See the package-registry invariant above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 03a3bcb: scoped the package-registry invariant to runtime engine defaults and documented the Copilot threat-detection exception for registry.npmjs.org.

var engineDefaultDomainSets = map[string][]string{
"claude": {
"*.githubusercontent.com",
"anthropic.com",
"api.anthropic.com",
"api.github.com",
"api.snapcraft.io",
"archive.ubuntu.com",
"azure.archive.ubuntu.com",
"cdn.playwright.dev",
"codeload.github.com",
"crl.geotrust.com",
"crl.globalsign.com",
"crl.identrust.com",
"crl.sectigo.com",
"crl.thawte.com",
"crl.usertrust.com",
"crl.verisign.com",
"crl3.digicert.com",
"crl4.digicert.com",
"crls.ssl.com",
"ghcr.io",
"github-cloud.githubusercontent.com",
"github-cloud.s3.amazonaws.com",
"github.com",
"host.docker.internal",
"json-schema.org",
"json.schemastore.org",
"keyserver.ubuntu.com",
"lfs.github.com",
"objects.githubusercontent.com",
"ocsp.digicert.com",
"ocsp.geotrust.com",
"ocsp.globalsign.com",
"ocsp.identrust.com",
"ocsp.sectigo.com",
"ocsp.ssl.com",
"ocsp.thawte.com",
"ocsp.usertrust.com",
"ocsp.verisign.com",
"packagecloud.io",
"packages.cloud.google.com",
"packages.microsoft.com",
"playwright.download.prss.microsoft.com",
"ppa.launchpad.net",
"raw.githubusercontent.com",
"s.symcb.com",
"s.symcd.com",
"security.ubuntu.com",
"sentry.io",
"statsig.anthropic.com",
"ts-crl.ws.symantec.com",
"ts-ocsp.ws.symantec.com",
},
"codex": {
"172.30.0.1", // AWF gateway IP - Codex resolves host.docker.internal to this IP for Rust DNS compatibility
"api.github.com", // Codex startup performs GitHub plugin sync requests against the GitHub API
"api.openai.com",
"chatgpt.com", // Codex CLI connects to chatgpt.com (and subdomains e.g. ab.chatgpt.com) for auth/telemetry
"github.com", // Codex startup accesses GitHub-hosted plugin metadata pages
"host.docker.internal",
"openai.com",
},
"copilot": {
"api.github.com",
"api.githubcopilot.com",
"github.com",
"host.docker.internal",
"raw.githubusercontent.com",
},
"gemini": {
"*.googleapis.com",
"generativelanguage.googleapis.com",
"github.com",
"host.docker.internal",
"raw.githubusercontent.com",
},
"pi": {
"api.githubcopilot.com", // Default provider (Copilot routing)
"host.docker.internal",
"github.com",
"raw.githubusercontent.com",
},
"pi-base": {
"host.docker.internal", // MCP gateway / API proxy access
"github.com",
"raw.githubusercontent.com",
},
"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",
"telemetry.enterprise.githubcopilot.com",
},
}

// GetEngineDefaultDomainSets returns copies of the named engine domain sets for
// analysis and reporting. Engine domain sets are not valid network.allowed values.
func GetEngineDefaultDomainSets() map[string][]string {
sets := make(map[string][]string, len(engineDefaultDomainSets))
for name, domains := range engineDefaultDomainSets {
sets[name] = copyEngineDefaultDomainSet(domains)
}
return sets
}

func copyEngineDefaultDomainSet(domains []string) []string {
return append([]string(nil), domains...)
}

// CopilotDefaultDomains are the default domains required for GitHub Copilot CLI authentication and operation.
//
// This list is limited to the shared gateway/GitHub transport baseline: the MCP/API gateway
// (host.docker.internal), the GitHub API and web hosts, and the Copilot routing hub used for
// inference. Plan-specific Copilot API hosts (business/enterprise/individual) and Copilot
// telemetry are *not* part of the default set: agents route inference through the AWF api-proxy,
// so those vendor hosts require an explicit `network: { allowed: [copilot-vendor] }` opt-in.
var CopilotDefaultDomains = []string{
"api.github.com",
"api.githubcopilot.com",
"github.com",
"host.docker.internal",
"raw.githubusercontent.com",
}
var CopilotDefaultDomains = copyEngineDefaultDomainSet(engineDefaultDomainSets["copilot"])

// CodexDefaultDomains are the minimal default domains required for Codex CLI operation
var CodexDefaultDomains = []string{
"172.30.0.1", // AWF gateway IP - Codex resolves host.docker.internal to this IP for Rust DNS compatibility
"api.github.com", // Codex startup performs GitHub plugin sync requests against the GitHub API
"api.openai.com",
"chatgpt.com", // Codex CLI connects to chatgpt.com (and subdomains e.g. ab.chatgpt.com) for auth/telemetry
"github.com", // Codex startup accesses GitHub-hosted plugin metadata pages
"host.docker.internal",
"openai.com",
}
// CodexDefaultDomains are the minimal default domains required for Codex CLI operation.
var CodexDefaultDomains = copyEngineDefaultDomainSet(engineDefaultDomainSets["codex"])

// ClaudeDefaultDomains are the default domains required for Claude Code CLI authentication and operation
var ClaudeDefaultDomains = []string{
"*.githubusercontent.com",
"anthropic.com",
"api.anthropic.com",
"api.github.com",
"api.snapcraft.io",
"archive.ubuntu.com",
"azure.archive.ubuntu.com",
"cdn.playwright.dev",
"codeload.github.com",
"crl.geotrust.com",
"crl.globalsign.com",
"crl.identrust.com",
"crl.sectigo.com",
"crl.thawte.com",
"crl.usertrust.com",
"crl.verisign.com",
"crl3.digicert.com",
"crl4.digicert.com",
"crls.ssl.com",
"ghcr.io",
"github-cloud.githubusercontent.com",
"github-cloud.s3.amazonaws.com",
"github.com",
"host.docker.internal",
"json-schema.org",
"json.schemastore.org",
"keyserver.ubuntu.com",
"lfs.github.com",
"objects.githubusercontent.com",
"ocsp.digicert.com",
"ocsp.geotrust.com",
"ocsp.globalsign.com",
"ocsp.identrust.com",
"ocsp.sectigo.com",
"ocsp.ssl.com",
"ocsp.thawte.com",
"ocsp.usertrust.com",
"ocsp.verisign.com",
"packagecloud.io",
"packages.cloud.google.com",
"packages.microsoft.com",
"playwright.download.prss.microsoft.com",
"ppa.launchpad.net",
"raw.githubusercontent.com",
"s.symcb.com",
"s.symcd.com",
"security.ubuntu.com",
"sentry.io",
"statsig.anthropic.com",
"ts-crl.ws.symantec.com",
"ts-ocsp.ws.symantec.com",
}
// ClaudeDefaultDomains are the default domains required for Claude Code CLI authentication and operation.
var ClaudeDefaultDomains = copyEngineDefaultDomainSet(engineDefaultDomainSets["claude"])

// GeminiDefaultDomains are the default domains required for Google Gemini CLI authentication and operation.
var GeminiDefaultDomains = []string{
"*.googleapis.com",
"generativelanguage.googleapis.com",
"github.com",
"host.docker.internal",
"raw.githubusercontent.com",
}
var GeminiDefaultDomains = copyEngineDefaultDomainSet(engineDefaultDomainSets["gemini"])

// PiBaseDefaultDomains are the base domains required for the Pi CLI to operate,
// independent of the chosen LLM provider. When a model uses provider/model format,
// provider-specific API domains are added on top via GetDefaultDomainsForEngine.
var PiBaseDefaultDomains = []string{
"host.docker.internal", // MCP gateway / API proxy access
"github.com",
"raw.githubusercontent.com",
}
var PiBaseDefaultDomains = copyEngineDefaultDomainSet(engineDefaultDomainSets["pi-base"])

// piProviderDomains maps provider prefixes to their API domains.
// Covers the same set of providers that Pi can route through via the AWF LLM gateway.
Expand All @@ -180,12 +226,7 @@ var piProviderDomains = map[string]string{
// PiDefaultDomains are the static default domains for backward compatibility when
// no model provider prefix is given. When a provider/model format is used, the
// dynamic path (GetDefaultDomainsForEngine) resolves provider-specific domains instead.
var PiDefaultDomains = []string{
"api.githubcopilot.com", // Default provider (Copilot routing)
"host.docker.internal",
"github.com",
"raw.githubusercontent.com",
}
var PiDefaultDomains = copyEngineDefaultDomainSet(engineDefaultDomainSets["pi"])

// extractProviderFromModel parses "provider/model" format and returns the
// lowercase provider prefix. Returns ("", nil) when no model is given or the
Expand Down Expand Up @@ -752,16 +793,16 @@ func GetAllowedDomainsForEngine(engine constants.EngineName, network *NetworkPer
}

// GetThreatDetectionAllowedDomains returns the minimal set of domains allowed for a Copilot
// detection run. It loads the "threat-detection" ecosystem from ecosystem_domains.json, which
// includes the Copilot API endpoints needed for read-only threat analysis plus registry.npmjs.org
// detection run. The "threat-detection" engine domain set includes the Copilot API endpoints
// needed for read-only threat analysis plus registry.npmjs.org
// for read-only npm package validation (e.g. verifying lockfile integrity hashes). It intentionally
// excludes raw.githubusercontent.com (not needed when MCP servers are disabled and the CLI binary
// is pre-installed). npm registry access is read-only metadata lookup only — installs are not
// permitted during detection runs.
// Any additional user-specified network.allowed entries are merged in (typically empty for detection).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Direct map access hands the internal slice to mergeDomainsWithNetworkToolsAndRuntimes — if that function appends to the slice in a future grow-in-place scenario, it could silently corrupt the registry entry for subsequent calls.

💡 Suggested fix

Use the same defensive copy pattern used everywhere else in this PR:

detectionDomains := copyEngineDefaultDomainSet(engineDefaultDomainSets["threat-detection"])

This is a one-liner and keeps the access pattern consistent with CopilotDefaultDomains, ClaudeDefaultDomains, etc.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 03a3bcb: GetThreatDetectionAllowedDomains now copies the threat-detection registry slice before merging additional network entries.

// Returns a deduplicated, sorted, comma-separated string suitable for AWF's --allow-domains flag.
func GetThreatDetectionAllowedDomains(network *NetworkPermissions) string {
detectionDomains := getEcosystemDomains("threat-detection")
detectionDomains := engineDefaultDomainSets["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)
Expand Down
27 changes: 26 additions & 1 deletion pkg/workflow/domains_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ func TestCopilotVendorDomainsRequireOptIn(t *testing.T) {
}

func TestThreatDetectionDomains(t *testing.T) {
detectionDomains := getEcosystemDomains("threat-detection")
detectionDomains := GetEngineDefaultDomainSets()["threat-detection"]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 03a3bcb: after preserving compatibility, the test now asserts network.allowed: [threat-detection] expands to the centralized Copilot detection domain set and stays in sync with the engine-default registry.


// Detection domains must include every required Copilot API domain
requiredDomains := []string{
Expand All @@ -423,6 +423,7 @@ func TestThreatDetectionDomains(t *testing.T) {
"registry.npmjs.org",
"telemetry.enterprise.githubcopilot.com",
}

detectionMap := make(map[string]bool)
for _, d := range detectionDomains {
detectionMap[d] = true
Expand All @@ -444,6 +445,30 @@ func TestThreatDetectionDomains(t *testing.T) {
"threat-detection ecosystem should have exactly %d entries", len(requiredDomains))
}

func TestGetEngineDefaultDomainSets(t *testing.T) {
sets := GetEngineDefaultDomainSets()

assert.Equal(t, CopilotDefaultDomains, sets["copilot"])
assert.Equal(t, ClaudeDefaultDomains, sets["claude"])
assert.Equal(t, CodexDefaultDomains, sets["codex"])
assert.Equal(t, GeminiDefaultDomains, sets["gemini"])
assert.Equal(t, PiBaseDefaultDomains, sets["pi-base"])
assert.Equal(t, PiDefaultDomains, sets["pi"])
assert.NotEmpty(t, sets["threat-detection"])

sets["copilot"][0] = "modified.example.com"
assert.NotEqual(t, "modified.example.com", CopilotDefaultDomains[0],
"callers must not be able to modify registered domain sets")

original := CopilotDefaultDomains[0]
t.Cleanup(func() {
CopilotDefaultDomains[0] = original
})
CopilotDefaultDomains[0] = "modified.example.com"
assert.Equal(t, original, GetEngineDefaultDomainSets()["copilot"][0],
"exported compatibility variables must not modify registered domain sets")
}

func TestGetThreatDetectionAllowedDomains(t *testing.T) {
// With empty network permissions the result equals the sorted detection domains
result := GetThreatDetectionAllowedDomains(&NetworkPermissions{Allowed: []string{}})
Expand Down
Loading