Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/enclave-force-public-repos.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/public/schemas/mcp-gateway-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@
},
"forcePublicRepos": {
"type": "boolean",
"description": "When true (default), forces the allow-only policy to repos=\"public\" at runtime if the gateway detects it is running in a public repository. Set to false by the compiler when private-to-public-flows: allow is declared in workflow frontmatter, or via MCP_GATEWAY_FORCE_PUBLIC_REPOS=false environment variable. See MCP Gateway Specification section 4.1.3.8.",
"description": "When true (default), forces the allow-only policy to repos=\"public\" at runtime if the gateway detects it is running in a public repository. Set to false by the compiler when private-to-public-flows: allow is declared in workflow frontmatter or when the GitHub MCP backend serves only a static agent enclave, or via MCP_GATEWAY_FORCE_PUBLIC_REPOS=false environment variable. See MCP Gateway Specification section 4.1.3.8.",
"default": true
},
"sinkVisibilityExemptServers": {
Expand Down
4 changes: 4 additions & 0 deletions docs/src/content/docs/experimental/enclaves.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ enclaves:
- GraphQL, search, writes, and every other GitHub tool remain denied.
- The minimum supported versions are AWF `v0.28.9` (or `v0.28.14` when using `trusted`) and mcpg `v0.4.15`.

Set `tools.github: false` so the GitHub MCP server is rendered for the enclave identity only. When primary-agent GitHub access is disabled, the compiler emits `"forcePublicRepos": false` in the gateway config: the gateway's runtime public-repos override would otherwise rewrite the enclave's allow-only scope to `repos: "public"` in a public repository, silently discarding `allowed-repos` and leaving the enclave with nothing to read. Disclosure stays bounded by the enclave's sensitivity ledger, `max-output-bytes`, and `max-invocations`, and safe outputs keep their explicit `sink-visibility` enforcement.

If the primary agent also enables GitHub MCP access (`tools.github` other than `gh-proxy`), the override must stay on to protect the primary read path, and the compiler warns that the enclave cannot read the declared private repositories in a public repository.

## Dynamic agent repository policies

Agent enclaves can admit one repository per invocation at runtime without listing every repository in frontmatter. Dynamic entries use the same `awf-enclave` MCP backend, but replace static `repos` with a closed compiler-owned policy envelope:
Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/reference/mcp-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ The `gateway` section is required and configures gateway-specific behavior:
| `keepaliveInterval` | integer | No | Keepalive ping interval in seconds for HTTP MCP backends. Prevents session expiry during long-running tasks. Use `-1` to disable, `0` or unset for gateway default (1500s = 25 min), or a positive integer for a custom interval. |
| `sessionTimeout` | string | No | Session timeout for MCP gateway sessions as a Go duration string (e.g. `"30m"`, `"4h"`, `"24h"`). Empty or omitted uses the gateway default (6h). Must be at least 5m when set by the workflow compiler (no upper bound; infrastructure operators may override via `MCP_GATEWAY_SESSION_TIMEOUT` env var). |
| `opentelemetry` | object | No | OpenTelemetry configuration for emitting distributed tracing events for MCP calls. See Section 4.1.3.7 for details. |
| `forcePublicRepos` | boolean | No | When `true` (default), forces the allow-only policy to `repos="public"` at runtime if the gateway detects it is running in a public repository. When `false`, disables this override — set by the compiler when `private-to-public-flows: allow` is declared in workflow frontmatter. See Section 4.1.3.8 for details. |
| `forcePublicRepos` | boolean | No | When `true` (default), forces the allow-only policy to `repos="public"` at runtime if the gateway detects it is running in a public repository. When `false`, disables this override — set by the compiler for `private-to-public-flows: allow` or an enclave-only static GitHub agent backend. See Section 4.1.3.8 for details. |
| `sinkVisibilityExemptServers` | array[string] | No | List of server IDs exempt from the default `sink-visibility="public"` enforcement. Use `["*"]` to exempt all servers. Set by the compiler when `private-to-public-flows` lists specific server IDs in workflow frontmatter. See Section 10.9 for details. |

*Exactly one of `agentId` or `agentIds` MUST be specified; the two fields are mutually exclusive. See Section 4.1.3.9.
Expand Down Expand Up @@ -578,7 +578,7 @@ When `forcePublicRepos` is `true` (the default), the gateway overrides the compi
- This override is **skipped** when `GITHUB_REPOSITORY` or the GitHub token is unavailable.
- API errors during visibility detection result in a non-fatal warning; the gateway/proxy falls back to the compiled policy.

**Opt-out**: Workflow authors who intentionally allow private→public data flows set `private-to-public-flows: allow` in frontmatter (Section 10.9). The compiler translates this to `gateway.forcePublicRepos: false` in the generated gateway JSON stdin config. For proxy mode, launchers should pass `--force-public-repos=false` when they need equivalent opt-out behavior.
**Opt-out**: Workflow authors who intentionally allow private→public data flows set `private-to-public-flows: allow` in frontmatter (Section 10.9). The compiler translates this to `gateway.forcePublicRepos: false` in the generated gateway JSON stdin config. The compiler also emits `gateway.forcePublicRepos: false` when the GitHub MCP server is rendered solely to serve a static agent enclave identity (`tools.github: false` plus `enclaves[].agent.tools.github`), because the override would otherwise discard the enclave's configured `allowed-repos`. For proxy mode, launchers should pass `--force-public-repos=false` when they need equivalent opt-out behavior.

**Environment variable override**: `MCP_GATEWAY_FORCE_PUBLIC_REPOS=false` disables the override without requiring a config change. The `--force-public-repos` flag defaults to the value of this environment variable (defaulting to `true` when unset).

Expand Down Expand Up @@ -2331,7 +2331,7 @@ Content-Type: application/json
- **Added**: Section 4.1.3.8 — `forcePublicRepos` Configuration
- New optional boolean gateway config field (default: `true`) that forces the allow-only policy to `repos="public"` at runtime when the gateway detects it is running in a public repository
- Prevents agents from accumulating private-data secrecy tags by restricting repository access at the input side
- Set to `false` by the compiler when `private-to-public-flows: allow` is declared in workflow frontmatter
- Set to `false` by the compiler when `private-to-public-flows: allow` is declared in workflow frontmatter or when the GitHub MCP backend serves only a static agent enclave
- Can also be overridden via `MCP_GATEWAY_FORCE_PUBLIC_REPOS=false` environment variable
- **Added**: `forcePublicRepos` field to the gateway configuration fields table (Section 4.1.3)
- **Added**: Section 10.8 — Write-Sink Guard Policy (`sink-visibility`)
Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/compiler_validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,17 @@ func (c *Compiler) validateCoreToolConfiguration(workflowData *WorkflowData, mar
return formatCompilerError(markdownPath, "error", err.Error(), err)
}
}
c.validateStaticEnclaveGitHubScopeOverrideWarning(workflowData)
return nil
}

func (c *Compiler) validateStaticEnclaveGitHubScopeOverrideWarning(workflowData *WorkflowData) {
if warning := staticEnclaveGitHubScopeOverrideWarning(workflowData); warning != "" {
fmt.Fprintln(os.Stderr, console.FormatWarningMessageStderr(warning))
c.IncrementWarningCount()
}
}

func validateGitHubCLIProxyVersion(workflowData *WorkflowData) error {
if !isGitHubCLIModeEnabled(workflowData) {
return nil
Expand Down
65 changes: 65 additions & 0 deletions pkg/workflow/enclave_github_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,46 @@ Read the private repository's issues through the enclave.
assert.Contains(t, lock, `"${AWF_ENCLAVE_GITHUB_MCP_AGENT_ID}":{"servers":["github"],"tools":{"github":["list_issues","issue_read"]},"allow-only":{"min-integrity":"none","repos":["octo-org/private-service"]}}`)
assert.Contains(t, lock, `export GH_AW_MCP_GITHUB_CHECK_AGENT_ID="${AWF_ENCLAVE_GITHUB_MCP_AGENT_ID}"`)
assert.NotContains(t, lock, `"${MCP_GATEWAY_AGENT_ID}":{"servers":["awf-enclave","github"`)

// The gateway's runtime forcePublicRepos override would rewrite the enclave's
// allow-only scope to repos="public" in a public repository, discarding
// allowed-repos and leaving the enclave with nothing to read.
assert.Contains(t, lock, `"forcePublicRepos": false`)
}

// TestBuildMCPGatewayConfigForcePublicReposForStaticEnclave verifies that the gateway's
// runtime public-repos override is disabled when the GitHub MCP server exists solely to
// serve a static enclave agent identity, and left at its default when the primary agent
// also has GitHub MCP access.
func TestBuildMCPGatewayConfigForcePublicReposForStaticEnclave(t *testing.T) {
t.Run("enclave-only GitHub backend disables the override", func(t *testing.T) {
data := enclaveGitHubToolsWorkflowData()
delete(data.Tools, "github")
data.ExplicitlyDisabledTools = map[string]struct{}{"github": {}}

cfg := buildMCPGatewayConfig(data)
require.NotNil(t, cfg)
require.NotNil(t, cfg.ForcePublicRepos, "ForcePublicRepos must be set for an enclave-only GitHub backend")
assert.False(t, *cfg.ForcePublicRepos)
})

t.Run("primary GitHub access keeps the override enabled", func(t *testing.T) {
data := enclaveGitHubToolsWorkflowData()
data.Tools["github"] = map[string]any{}

cfg := buildMCPGatewayConfig(data)
require.NotNil(t, cfg)
assert.Nil(t, cfg.ForcePublicRepos, "ForcePublicRepos must stay at the gateway default when the primary agent reads GitHub")
})

t.Run("workflows without enclaves keep the override enabled", func(t *testing.T) {
data := enclaveWorkflowData(true, false, 120, 0)
data.Tools["github"] = map[string]any{}

cfg := buildMCPGatewayConfig(data)
require.NotNil(t, cfg)
assert.Nil(t, cfg.ForcePublicRepos)
})
}

func TestGitHubGuardPoliciesFromStepSkipsEnclaveOnlyBackend(t *testing.T) {
Expand Down Expand Up @@ -436,3 +476,28 @@ func TestEnclaveGitHubToolsVersionGates(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), string(constants.MCPGEnclaveAgentToolsMinVersion))
}

// TestStaticEnclaveGitHubScopeOverrideWarning verifies that combining a static GitHub agent
// enclave with primary-agent GitHub access is reported at compile time, because the gateway's
// forcePublicRepos override cannot be disabled without also relaxing the primary read path.
func TestStaticEnclaveGitHubScopeOverrideWarning(t *testing.T) {
data := enclaveGitHubToolsWorkflowData()
delete(data.Tools, "github")
data.ExplicitlyDisabledTools = map[string]struct{}{"github": {}}
assert.Empty(t, staticEnclaveGitHubScopeOverrideWarning(data))

data.Tools["github"] = map[string]any{}
assert.Contains(t, staticEnclaveGitHubScopeOverrideWarning(data), "static GitHub agent enclave is combined with primary 'tools.github'")

assert.Empty(t, staticEnclaveGitHubScopeOverrideWarning(enclaveWorkflowData(true, false, 120, 0)))
}

func TestStaticEnclaveGitHubScopeOverrideWarningIncrementsWarningCount(t *testing.T) {
compiler := NewCompiler()
data := enclaveGitHubToolsWorkflowData()
data.Tools["github"] = map[string]any{}

require.NoError(t, compiler.validateCoreToolConfiguration(data, ""))

assert.Equal(t, 1, compiler.GetWarningCount())
}
18 changes: 18 additions & 0 deletions pkg/workflow/enclaves.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,24 @@ func validateEnclavesConfig(workflowData *WorkflowData) error {
return nil
}

// staticEnclaveGitHubScopeOverrideWarning returns a warning when a static GitHub agent
// enclave shares the GitHub MCP server with a GitHub-enabled primary agent, and an empty
// string otherwise. The gateway's forcePublicRepos safety net can only be disabled for the
// whole gateway, so it must stay enabled to protect the primary agent's read path. In a
// public repository that override rewrites the enclave's allow-only scope to repos="public",
// which silently discards enclaves[].agent.tools.github.allowed-repos and leaves the enclave
// unable to read the declared private repositories.

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.

L363: shrink: 7-line explanatory warning prose in code comments. Move rationale to docs and keep a 1-line function comment plus concise warning string.

func staticEnclaveGitHubScopeOverrideWarning(workflowData *WorkflowData) string {
if !enclaveGitHubIssuesEnabled(workflowData) || !primaryGitHubMCPEnabled(workflowData) {

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] Good targeted fix — this warning correctly names the discarded config path instead of failing silently, consistent with the codebase's existing pattern (see console.FormatWarningMessageStderr in agent_validation.go, compiler.go). One gap worth flagging for a follow-up: the dynamic-enclave delegation path (githubBackendIsDynamicDelegationOnly) shares the same forcePublicRepos-clobbers-enclave-scope risk as the static path fixed here, but neither buildMCPGatewayConfig nor this warning covers it — combining a dynamic GitHub enclave with primary tools.github would silently rewrite the delegated identity's allow-only scope the same way. Not blocking this PR (out of its stated scope), but worth a tracking issue so the dynamic path isn't left with the same silent-failure gap this PR just closed for static enclaves.

@copilot please address this.

return ""
}
return "enclaves: a static GitHub agent enclave is combined with primary 'tools.github'. In a public repository " +

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.

This warning is a false positive for the documented tools.github.private-to-public-flows: allow opt-out, because buildMCPGatewayConfig already emits forcePublicRepos: false for that configuration and the enclave keeps its declared scope.

💡 Why this blocks the change

staticEnclaveGitHubScopeOverrideWarning only checks "static enclave + primary tools.github". It never checks whether the workflow has already disabled the runtime override via private-to-public-flows: allow, so the compiler will still print "the enclave reads nothing" and tell authors to remove tools.github even when the chosen configuration is the supported escape hatch.

Please key this warning off the effective forcePublicRepos state instead of the raw tool combination, and add a regression test for tools.github + static enclave + private-to-public-flows: allow so the warning only appears when the override really stays enabled.

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.

This warning is emitted unconditionally for any static-enclave-plus-primary-tools.github workflow, but the breakage you describe only occurs in public repositories, so private-repo compiles now get a false alarm too.

💡 Why this blocks the change

The warning text says the gateway rewrites the scope to repos="public" and leaves the enclave reading nothing, but that runtime override only does that on public repositories. On a private repository, this exact configuration does not hit the stated failure mode, so emitting the warning at compile time as a guaranteed problem is inaccurate and trains users to ignore it.

Please either gate the diagnostic on a public-repository context when that is knowable, or weaken the message so it clearly states this is a public-repo-only hazard instead of a universal failure.

"the MCP gateway forces the GitHub allow-only scope to repos=\"public\", discarding the enclave scope from " +
"enclaves[].agent.tools.github.allowed-repos (or enclaves[].repos when it is omitted), so the enclave reads " +
"nothing. Remove primary 'tools.github' (set 'github: false') so the GitHub MCP server serves the enclave " +
"identity only."
}

func validateEnclaveEntry(index int, enclave *EnclaveConfig, seenTypes map[string]struct{}, repositorySensitivities map[string]string) error {
if enclave == nil {
return fmt.Errorf("enclaves[%d] must be an object. Example:\n\nenclaves:\n - script:\n repos:\n - repo: org/my-repo\n sensitivity: confidential", index)
Expand Down
15 changes: 15 additions & 0 deletions pkg/workflow/mcp_gateway_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,21 @@ func buildMCPGatewayConfig(workflowData *WorkflowData) *MCPGatewayRuntimeConfig
}
}

// A static GitHub agent enclave is the mechanism that makes private-to-public

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.

L221: yagni: long policy narrative embedded in runtime builder comment. Keep intent in docs/spec and reduce to a short guard comment next to the condition.

// disclosure safe: reads happen inside an isolated executor and are bounded by the
// sensitivity ledger, max-output-bytes and max-invocations. When the GitHub MCP
// server is rendered solely to serve that enclave identity, the gateway's runtime
// forcePublicRepos override would rewrite the enclave's allow-only scope to
// repos="public" in a public repository, silently discarding the configured
// allowed-repos and leaving the enclave with nothing to read. Disable the override
// for that case: the primary agent has no GitHub read path at all here, and every
// write-sink guard policy emitted for this configuration carries an explicit
// sink-visibility, so safe-outputs enforcement is unaffected.
if forcePublicRepos == nil && githubBackendIsStaticEnclaveDelegationOnly(workflowData) {
falseVal := false
forcePublicRepos = &falseVal
}

config := &MCPGatewayRuntimeConfig{
Port: int(DefaultMCPGatewayPort), // Will be formatted as "${MCP_GATEWAY_PORT}" in renderer
Domain: "${MCP_GATEWAY_DOMAIN}", // Gateway variable expression
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/schemas/mcp-gateway-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@
},
"forcePublicRepos": {
"type": "boolean",
"description": "When true (default), forces the allow-only policy to repos=\"public\" at runtime if the gateway detects it is running in a public repository. Set to false by the compiler when private-to-public-flows: allow is declared in workflow frontmatter, or via MCP_GATEWAY_FORCE_PUBLIC_REPOS=false environment variable. See MCP Gateway Specification section 4.1.3.8.",
"description": "When true (default), forces the allow-only policy to repos=\"public\" at runtime if the gateway detects it is running in a public repository. Set to false by the compiler when private-to-public-flows: allow is declared in workflow frontmatter or when the GitHub MCP backend serves only a static agent enclave, or via MCP_GATEWAY_FORCE_PUBLIC_REPOS=false environment variable. See MCP Gateway Specification section 4.1.3.8.",
"default": true
},
"sinkVisibilityExemptServers": {
Expand Down
Loading