From d8dfdc70077ea0e430906d7b15eb6579b35c05f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:39:03 +0000 Subject: [PATCH 1/6] Initial plan From 1400cbe059db0d76a87d252ef0021aa53e1bf763 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:57:35 +0000 Subject: [PATCH 2/6] docs: add sink-visibility and write-sink guard policy to MCP Gateway spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Section 4.1.3.8 documenting the new `forcePublicRepos` gateway config field - Add `forcePublicRepos` to the gateway config fields table (Section 4.1.3) - Add Section 10.8 — Write-Sink Guard Policy with full `sink-visibility` field specification, enforcement model, DIFC semantics, runtime verification, and compiler behavior - Add Section 10.9 — Cross-Visibility Opt-Out documenting `private-to-public-flows: allow` frontmatter option, its constraints, compiler responsibilities, and interaction matrix - Add compliance test category 11.1.12 with T-WS-001 through T-WS-008 - Update compliance checklist with Write-Sink Guard Policy row - Add changelog entry for spec version 1.15.0 - Update both JSON schema files (docs/public and pkg/workflow) with: - New `writeSinkGuardPolicyConfig` definition with `accept` and `sink-visibility` fields - Typed `write-sink` property in `guard-policies` for both stdioServerConfig and httpServerConfig - New `forcePublicRepos` boolean field in `gatewayConfig` - Added `guard-policies` to `stdioServerConfig` in docs/public schema (was missing) Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .../schemas/mcp-gateway-config.schema.json | 44 +++- .../src/content/docs/reference/mcp-gateway.md | 203 +++++++++++++++++- .../schemas/mcp-gateway-config.schema.json | 39 +++- 3 files changed, 280 insertions(+), 6 deletions(-) diff --git a/docs/public/schemas/mcp-gateway-config.schema.json b/docs/public/schemas/mcp-gateway-config.schema.json index 7830d80ab8d..675dc255089 100644 --- a/docs/public/schemas/mcp-gateway-config.schema.json +++ b/docs/public/schemas/mcp-gateway-config.schema.json @@ -116,6 +116,16 @@ "type": "string" }, "default": ["*"] + }, + "guard-policies": { + "type": "object", + "description": "Guard policies for access control at the MCP gateway level. Supports a 'write-sink' policy for DIFC-based output filtering. Additional server-specific policies may be provided.", + "properties": { + "write-sink": { + "$ref": "#/definitions/writeSinkGuardPolicyConfig" + } + }, + "additionalProperties": true } }, "required": ["container"], @@ -163,7 +173,12 @@ }, "guard-policies": { "type": "object", - "description": "Guard policies for access control at the MCP gateway level. The structure of guard policies is server-specific. For GitHub MCP server, see the GitHub guard policy schema. For other servers (Jira, WorkIQ), different policy schemas will apply.", + "description": "Guard policies for access control at the MCP gateway level. Supports a 'write-sink' policy for DIFC-based output filtering. Additional server-specific policies may be provided.", + "properties": { + "write-sink": { + "$ref": "#/definitions/writeSinkGuardPolicyConfig" + } + }, "additionalProperties": true }, "auth": { @@ -282,14 +297,37 @@ "opentelemetry": { "$ref": "#/definitions/opentelemetryConfig", "description": "Optional OpenTelemetry configuration for emitting distributed tracing spans for MCP calls. When configured, the gateway exports OTLP/HTTP traces to the specified collector endpoint." + }, + "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.", + "default": true } }, "required": ["port", "domain", "apiKey"], "additionalProperties": false }, - "opentelemetryConfig": { + "writeSinkGuardPolicyConfig": { "type": "object", - "description": "OpenTelemetry configuration for the MCP Gateway. When present, the gateway emits distributed tracing spans for each MCP tool invocation and exports them via OTLP/HTTP to the configured collector endpoint. Per MCP Gateway Specification section 4.1.3.6.", + "description": "Write-sink guard policy for DIFC-based output filtering. Controls whether an agent may write to the safe-outputs sink based on the agent's accumulated secrecy tags and the target repository visibility. Per MCP Gateway Specification section 10.8.", + "properties": { + "accept": { + "type": "array", + "description": "Secrecy tag patterns that are permitted to write to this sink. Use [\"*\"] to accept all secrecy levels. Required for all write-sink policies. When sink-visibility is \"public\", this field is syntactically required but has no runtime effect — resource secrecy is unconditionally set to empty.", + "items": { + "type": "string" + } + }, + "sink-visibility": { + "type": "string", + "description": "Declares the visibility of the safe-outputs target repository (always the workflow's own repo, i.e. GITHUB_REPOSITORY). When \"public\", agents with non-empty secrecy are blocked regardless of accept patterns. When \"private\" or \"internal\", standard accept-pattern matching applies. When omitted, backward-compatible accept-pattern matching applies.", + "enum": ["public", "private", "internal"] + } + }, + "required": ["accept"], + "additionalProperties": false + }, + "opentelemetryConfig": { "properties": { "endpoint": { "type": "string", diff --git a/docs/src/content/docs/reference/mcp-gateway.md b/docs/src/content/docs/reference/mcp-gateway.md index 50f5839937b..0d18325ae1c 100644 --- a/docs/src/content/docs/reference/mcp-gateway.md +++ b/docs/src/content/docs/reference/mcp-gateway.md @@ -7,7 +7,7 @@ sidebar: # MCP Gateway Specification -**Version**: 1.14.0 +**Version**: 1.15.0 **Status**: Draft Specification **Latest Version**: [mcp-gateway](/gh-aw/reference/mcp-gateway/) **JSON Schema**: [mcp-gateway-config.schema.json](/gh-aw/schemas/mcp-gateway-config.schema.json) @@ -254,6 +254,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. | #### 4.1.3.1 Payload Directory Path Validation @@ -556,6 +557,40 @@ The gateway MUST NOT fail to start if the OpenTelemetry collector endpoint is un **Compliance Test**: T-OTEL-001 through T-OTEL-010 (Section 11.1.10) +#### 4.1.3.8 `forcePublicRepos` Configuration + +When `forcePublicRepos` is `true` (the default), the gateway overrides the compiled allow-only policy to `repos="public"` at startup if it detects the workflow is running in a public repository. This prevents agents from accumulating private-data secrecy tags in the first place — even when the compiled config grants broader access. + +**Detection mechanism**: The gateway reads `GITHUB_REPOSITORY` and calls `GET /repos/{owner}/{repo}` at startup to determine repository visibility. If the repository is public and `forcePublicRepos` is `true`, the override is applied unconditionally. + +**Scope of the override**: Only the allow-only policy is affected. Guard policies, tool permissions, and other configuration are unchanged. + +**Precedence rules**: +- Configured `repos="public"` (explicit in the allow-only policy) is always preserved — the gateway never relaxes an explicit public constraint. +- Runtime detection of a public repo with `forcePublicRepos: true` **adds** the public restriction; it does not relax any existing constraint. +- `forcePublicRepos: false` disables the runtime override entirely — the compiled allow-only policy is used as-is. +- 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 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 `forcePublicRepos: false` in the generated gateway config. See Section 10.8 and 10.9 for the write-sink guard policy interaction. + +**Environment variable override**: `MCP_GATEWAY_FORCE_PUBLIC_REPOS=false` disables the override without requiring a config change. + +**Configuration Example**: + +```json +{ + "gateway": { + "port": 8080, + "domain": "localhost", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "forcePublicRepos": false + } +} +``` + +**Compliance Test**: T-WS-004 (Section 11.1.12) + #### 4.1.3a Top-Level Configuration Fields The following fields MAY be specified at the top level of the configuration: @@ -1533,6 +1568,135 @@ Each guard policy list field (`blocked-users`, `trusted-users`, `approval-labels Variables are split on commas and newlines, trimmed of whitespace, and deduplicated. The union of workflow-declared values and variable values forms the effective list used at runtime. +### 10.8 Write-Sink Guard Policy (`sink-visibility`) + +The write-sink guard policy controls whether an agent may write to the safe-outputs sink based on **DIFC secrecy** — a data-flow integrity property that prevents private-origin data from leaking into public repositories. This addresses the GitLost vulnerability class, where an agent accumulates private-data secrecy tags by reading private repositories and then writes that tainted content to a public output. + +#### 10.8.1 Overview + +The write-sink guard policy applies at the **output side**: it is checked immediately before the safe-outputs MCP server accepts a write operation. Unlike the integrity guard policy (Section 10), which filters *inputs* to the agent, the write-sink guard filters *outputs* from the agent. + +**Key assumption**: The safe-outputs target is always the workflow's own repository (`GITHUB_REPOSITORY`). Under this assumption, "sink visibility" and "workflow repository visibility" are the same value. The runtime determines sink visibility by reading `GITHUB_REPOSITORY` and calling `GET /repos/{owner}/{repo}` at startup. + +#### 10.8.2 `sink-visibility` Field + +The `write-sink` object inside `guard-policies` for a safe-outputs (or equivalent) MCP server accepts the following field: + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `accept` | array[string] | Yes | — | Secrecy tag patterns that are permitted to write to this sink. Use `["*"]` to accept all secrecy levels. | +| `sink-visibility` | string | No | (omitted) | Declares the visibility of the safe-outputs target repository. Values: `"public"`, `"private"`, `"internal"`. When `"public"`, any agent with non-empty secrecy is blocked. | + +**Values**: + +| Value | Enforcement | +|-------|-------------| +| `"public"` | Target is a public repo; agents with **non-empty secrecy** are BLOCKED. Resource secrecy is unconditionally set to empty (`{}`), overriding `accept` patterns. | +| `"private"` | Target is a private repo; standard `accept`-pattern matching applies. | +| `"internal"` | Target is an org-internal repo; semantically equivalent to `"private"` — standard `accept`-pattern matching applies. | +| (omitted) | Backward-compatible default; standard `accept`-pattern matching applies. | + +**Enforcement model**: Only `"public"` alters DIFC enforcement. Both `"private"` and `"internal"` are semantically equivalent to each other and to the omitted case. The `"internal"` value exists for configuration fidelity (matching GitHub's three-tier visibility model) rather than distinct enforcement behavior. Implementations MUST treat any non-`"public"` value as "use accept patterns normally." + +#### 10.8.3 Interaction with `accept` + +- When `sink-visibility` is `"public"`: the `accept` array is **ignored for enforcement** — resource secrecy is unconditionally set to empty (`{}`). The DIFC write check (`agentSecrecy ⊆ resourceSecrecy`) fails for any agent with non-empty secrecy. `accept` is still syntactically required by validation but has no runtime effect. +- When `sink-visibility` is `"private"`, `"internal"`, or omitted: `accept` is the sole determinant of resource secrecy and controls which agents may write to the sink. + +**Precedence**: `sink-visibility: "public"` is a hard override that trumps `accept`. + +#### 10.8.4 Runtime Verification (Defense-in-Depth) + +The gateway performs a runtime visibility check at startup as defense-in-depth: + +1. Reads `GITHUB_REPOSITORY` and calls `GET /repos/{owner}/{repo}` to verify actual visibility. +2. If the repo is public but `sink-visibility` is set to `"private"` or `"internal"` → overrides `sink-visibility` to `"public"` with a warning. +3. **Skipped** when `sink-visibility` is omitted (preserves backward compatibility). +4. **Skipped** when `GITHUB_REPOSITORY` or the GitHub token is unavailable. +5. Falls back to the configured value on API errors (non-fatal); this is logged as a warning. +6. Never relaxes: a configured `"public"` stays `"public"` even if the repo is actually private. + +#### 10.8.5 Compiler Behavior + +The gh-aw compiler automatically sets `sink-visibility` as a runtime expression in the generated gateway config when the GitHub tool is configured with safe-outputs: + +``` +"sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} +``` + +This runtime expression resolves to the actual repository visibility at workflow execution time (as determined by the automatic lockdown detection step). Workflow authors do not need to set `sink-visibility` manually — it is derived from the repository context automatically. + +**Configuration Example** (as rendered in the generated config): + +```json +{ + "mcpServers": { + "safe-outputs": { + "container": "ghcr.io/github/safe-outputs:latest", + "guard-policies": { + "write-sink": { + "accept": ["*"], + "sink-visibility": "public" + } + } + } + } +} +``` + +**Compliance Tests**: T-WS-001 through T-WS-003 (Section 11.1.12) + +### 10.9 Cross-Visibility Opt-Out (`private-to-public-flows: allow`) + +Workflow authors who intentionally allow private→public data flows can opt out of both cross-visibility protections (the `forcePublicRepos` override in Section 4.1.3.8 and the `sink-visibility` runtime verification in Section 10.8.4) by declaring `private-to-public-flows: allow` in workflow frontmatter. + +#### 10.9.1 Workflow Frontmatter + +```yaml +--- +tools: + - github + - safe-outputs +private-to-public-flows: allow +--- +``` + +#### 10.9.2 Constraints + +- **Incompatible with `guards_mode: strict`** — the compiler MUST reject this combination at compile time with an error. +- When paired with non-strict mode (`filter` or `propagate`), disables both: + - Forced `repos="public"` override (Section 4.1.3.8) + - `sink-visibility` runtime verification override (Section 10.8.4) + +#### 10.9.3 Compiler Responsibilities + +When the compiler encounters `private-to-public-flows: allow`: + +1. **Validate**: If `guards_mode` is `strict`, emit a compile error. +2. **Emit config**: Set `gateway.forcePublicRepos: false` in the generated JSON stdin config. +3. **Skip sink-visibility override**: Do not set `sink-visibility: "public"` even if the target repo is public at compile time. +4. **Audit trail**: Log that the workflow author opted out of cross-visibility protection. + +#### 10.9.4 Interaction Matrix + +| `guards_mode` | `private-to-public-flows` | Forced `repos=public` | `sink-visibility` enforced | +|---|---|---|---| +| `strict` | `allow` | ❌ **Rejected** (compile error) | — | +| `strict` | *(omitted)* | ✅ Yes | ✅ Yes | +| `filter` / `propagate` | `allow` | ❌ Disabled | ❌ Disabled | +| `filter` / `propagate` | *(omitted)* | ✅ Yes | ✅ Yes | + +#### 10.9.5 Security Rationale + +This opt-out exists for workflows that legitimately need to: +- Read from private repos and post summaries to public issue trackers +- Aggregate private data into public dashboards +- Cross-post between private and public repos + +The strict-mode incompatibility ensures organizations requiring maximum security cannot accidentally enable this escape hatch. + +**Compliance Test**: T-WS-005 (Section 11.1.12) + --- ## 11. Compliance Testing @@ -1668,6 +1832,17 @@ A conforming implementation MUST pass the following test categories: - **T-GP-009**: `refusal-labels` accepts a GitHub Actions expression (comma- or newline-separated list) - **T-GP-010**: `min-integrity: none` allows items at `none` integrity through; items downgraded by `refusal-labels` to `none` are visible when `min-integrity: none` +#### 11.1.12 Write-Sink Guard Policy Tests + +- **T-WS-001**: Agent with non-empty secrecy is blocked when `sink-visibility` is `"public"` — DIFC write check fails regardless of `accept` patterns +- **T-WS-002**: Agent with empty secrecy is allowed when `sink-visibility` is `"public"` — DIFC write check passes +- **T-WS-003**: `accept` patterns are used normally when `sink-visibility` is `"private"`, `"internal"`, or omitted +- **T-WS-004**: Gateway overrides allow-only policy to `repos="public"` at startup when repository is public and `forcePublicRepos` is `true` +- **T-WS-005**: `forcePublicRepos: false` disables the runtime override — compiled allow-only policy is used as-is +- **T-WS-006**: Runtime verification overrides `sink-visibility` to `"public"` when configured value is `"private"` or `"internal"` but actual repo is public +- **T-WS-007**: Runtime verification is skipped when `sink-visibility` is omitted (backward compatibility) +- **T-WS-008**: Configured `"public"` is never relaxed by runtime verification even if API returns private visibility + ### 11.2 Compliance Checklist | Requirement | Test ID | Level | Status | @@ -1685,6 +1860,7 @@ A conforming implementation MUST pass the following test categories: | Gateway lifecycle | T-LIFE-* | 2 | Standard | | OpenTelemetry | T-OTEL-* | 3 | Optional | | Guard policy | T-GP-* | 2 | Standard | +| Write-sink guard policy | T-WS-* | 2 | Standard | ### 11.3 Test Execution @@ -2022,6 +2198,31 @@ Content-Type: application/json ## Change Log +### Version 1.15.0 (Draft) + +- **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 + - 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`) + - Formal specification of the `write-sink` guard policy for DIFC-based output filtering + - Section 10.8.1 — Overview: describes the write-sink guard as output-side filtering complementary to input-side integrity filtering + - Section 10.8.2 — `sink-visibility` field reference: values `"public"`, `"private"`, `"internal"`, and omitted; enforcement model per value + - Section 10.8.3 — Interaction with `accept`: `"public"` unconditionally overrides accept patterns; non-public values delegate to accept patterns + - Section 10.8.4 — Runtime Verification (defense-in-depth): gateway reads `GITHUB_REPOSITORY` at startup and overrides misconfigured `sink-visibility` when the actual repo is public; skipped when `sink-visibility` is omitted + - Section 10.8.5 — Compiler Behavior: documents the runtime expression emitted for `sink-visibility` using `determine-automatic-lockdown` step output +- **Added**: Section 10.9 — Cross-Visibility Opt-Out (`private-to-public-flows: allow`) + - Workflow frontmatter field that disables both `forcePublicRepos` override and `sink-visibility` runtime verification + - Section 10.9.2 — Constraints: incompatible with `guards_mode: strict` (compile error); only allowed with `filter`/`propagate` + - Section 10.9.3 — Compiler Responsibilities: validate, emit `forcePublicRepos: false`, skip `sink-visibility` override, and log opt-out + - Section 10.9.4 — Interaction Matrix: defines enforced/disabled combinations per `guards_mode` and `private-to-public-flows` setting + - Section 10.9.5 — Security Rationale: documents legitimate use cases for cross-visibility flows +- **Added**: Compliance test category 11.1.12 — Write-Sink Guard Policy Tests (T-WS-001 through T-WS-008) +- **Updated**: Compliance Checklist (Section 11.2) — added Write-Sink Guard Policy row (T-WS-*, Level 2, Standard) +- **Updated**: JSON Schema — added typed `write-sink` property to `guard-policies` in both `stdioServerConfig` and `httpServerConfig`; added `forcePublicRepos` property to `gatewayConfig` + ### Version 1.14.0 (Draft) - **Added**: Section 10 — Guard Policy diff --git a/pkg/workflow/schemas/mcp-gateway-config.schema.json b/pkg/workflow/schemas/mcp-gateway-config.schema.json index 191bedc20d9..28ba2eac85d 100644 --- a/pkg/workflow/schemas/mcp-gateway-config.schema.json +++ b/pkg/workflow/schemas/mcp-gateway-config.schema.json @@ -96,7 +96,12 @@ }, "guard-policies": { "type": "object", - "description": "Guard policies for access control at the MCP gateway level. The structure of guard policies is server-specific. For GitHub MCP server, see the GitHub guard policy schema. For other servers (Jira, WorkIQ), different policy schemas will apply.", + "description": "Guard policies for access control at the MCP gateway level. Supports a 'write-sink' policy for DIFC-based output filtering. Additional server-specific policies may be provided.", + "properties": { + "write-sink": { + "$ref": "#/definitions/writeSinkGuardPolicyConfig" + } + }, "additionalProperties": true } }, @@ -145,7 +150,12 @@ }, "guard-policies": { "type": "object", - "description": "Guard policies for access control at the MCP gateway level. The structure of guard policies is server-specific. For GitHub MCP server, see the GitHub guard policy schema. For other servers (Jira, WorkIQ), different policy schemas will apply.", + "description": "Guard policies for access control at the MCP gateway level. Supports a 'write-sink' policy for DIFC-based output filtering. Additional server-specific policies may be provided.", + "properties": { + "write-sink": { + "$ref": "#/definitions/writeSinkGuardPolicyConfig" + } + }, "additionalProperties": true }, "auth": { @@ -251,11 +261,36 @@ "opentelemetry": { "$ref": "#/definitions/opentelemetryConfig", "description": "Optional OpenTelemetry configuration for emitting distributed tracing spans for MCP calls. When configured, the gateway exports OTLP/HTTP traces to the specified collector endpoint." + }, + "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.", + "default": true } }, "required": ["port", "domain", "apiKey"], "additionalProperties": false }, + "writeSinkGuardPolicyConfig": { + "type": "object", + "description": "Write-sink guard policy for DIFC-based output filtering. Controls whether an agent may write to the safe-outputs sink based on the agent's accumulated secrecy tags and the target repository visibility. Per MCP Gateway Specification section 10.8.", + "properties": { + "accept": { + "type": "array", + "description": "Secrecy tag patterns that are permitted to write to this sink. Use [\"*\"] to accept all secrecy levels. Required for all write-sink policies. When sink-visibility is \"public\", this field is syntactically required but has no runtime effect — resource secrecy is unconditionally set to empty.", + "items": { + "type": "string" + } + }, + "sink-visibility": { + "type": "string", + "description": "Declares the visibility of the safe-outputs target repository (always the workflow's own repo, i.e. GITHUB_REPOSITORY). When \"public\", agents with non-empty secrecy are blocked regardless of accept patterns. When \"private\" or \"internal\", standard accept-pattern matching applies. When omitted, backward-compatible accept-pattern matching applies.", + "enum": ["public", "private", "internal"] + } + }, + "required": ["accept"], + "additionalProperties": false + }, "opentelemetryConfig": { "type": "object", "description": "OpenTelemetry configuration for the MCP Gateway. When present, the gateway emits distributed tracing spans for each MCP tool invocation and exports them via OTLP/HTTP to the configured collector endpoint. Per MCP Gateway Specification section 4.1.3.6.", From a20b4e26964ce313fa63b736ed009f9404335fcb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:58:29 +0000 Subject: [PATCH 3/6] fix: restore missing type and description in opentelemetryConfig definition Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- docs/public/schemas/mcp-gateway-config.schema.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/public/schemas/mcp-gateway-config.schema.json b/docs/public/schemas/mcp-gateway-config.schema.json index 675dc255089..731a88fc3c2 100644 --- a/docs/public/schemas/mcp-gateway-config.schema.json +++ b/docs/public/schemas/mcp-gateway-config.schema.json @@ -328,6 +328,8 @@ "additionalProperties": false }, "opentelemetryConfig": { + "type": "object", + "description": "OpenTelemetry configuration for the MCP Gateway. When present, the gateway emits distributed tracing spans for each MCP tool invocation and exports them via OTLP/HTTP to the configured collector endpoint. Per MCP Gateway Specification section 4.1.3.6.", "properties": { "endpoint": { "type": "string", From e6f943f1eaf0016d6cd7c8596ee5ea0ddf52e880 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:59:34 +0000 Subject: [PATCH 4/6] docs: clarify sink-visibility enforcement model and fallback behavior Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- docs/src/content/docs/reference/mcp-gateway.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/reference/mcp-gateway.md b/docs/src/content/docs/reference/mcp-gateway.md index 0d18325ae1c..f084d194b70 100644 --- a/docs/src/content/docs/reference/mcp-gateway.md +++ b/docs/src/content/docs/reference/mcp-gateway.md @@ -1596,7 +1596,7 @@ The `write-sink` object inside `guard-policies` for a safe-outputs (or equivalen | `"internal"` | Target is an org-internal repo; semantically equivalent to `"private"` — standard `accept`-pattern matching applies. | | (omitted) | Backward-compatible default; standard `accept`-pattern matching applies. | -**Enforcement model**: Only `"public"` alters DIFC enforcement. Both `"private"` and `"internal"` are semantically equivalent to each other and to the omitted case. The `"internal"` value exists for configuration fidelity (matching GitHub's three-tier visibility model) rather than distinct enforcement behavior. Implementations MUST treat any non-`"public"` value as "use accept patterns normally." +**Enforcement model**: Only `"public"` alters DIFC enforcement. Both `"private"` and `"internal"` are semantically equivalent to each other and to the omitted case. The `"internal"` value exists for configuration fidelity (matching GitHub's three-tier visibility model) rather than distinct enforcement behavior. Implementations MUST treat any non-`"public"` value or an omitted `sink-visibility` as "use accept patterns normally." #### 10.8.3 Interaction with `accept` @@ -1611,9 +1611,9 @@ The gateway performs a runtime visibility check at startup as defense-in-depth: 1. Reads `GITHUB_REPOSITORY` and calls `GET /repos/{owner}/{repo}` to verify actual visibility. 2. If the repo is public but `sink-visibility` is set to `"private"` or `"internal"` → overrides `sink-visibility` to `"public"` with a warning. -3. **Skipped** when `sink-visibility` is omitted (preserves backward compatibility). +3. **Skipped** when `sink-visibility` is omitted (preserves backward compatibility; omitted means no write-sink enforcement change). 4. **Skipped** when `GITHUB_REPOSITORY` or the GitHub token is unavailable. -5. Falls back to the configured value on API errors (non-fatal); this is logged as a warning. +5. Falls back to the configured value on API errors (non-fatal); this is logged as a warning. When `sink-visibility` is omitted, fallback means enforcement remains skipped. 6. Never relaxes: a configured `"public"` stays `"public"` even if the repo is actually private. #### 10.8.5 Compiler Behavior From 804b9639b07c46332620ccaaa25926971d304716 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:14:17 +0000 Subject: [PATCH 5/6] docs: fix grammar, wording, and contradiction in mcp-gateway spec Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/reference/mcp-gateway.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/reference/mcp-gateway.md b/docs/src/content/docs/reference/mcp-gateway.md index f084d194b70..8f51d2643ce 100644 --- a/docs/src/content/docs/reference/mcp-gateway.md +++ b/docs/src/content/docs/reference/mcp-gateway.md @@ -561,7 +561,7 @@ The gateway MUST NOT fail to start if the OpenTelemetry collector endpoint is un When `forcePublicRepos` is `true` (the default), the gateway overrides the compiled allow-only policy to `repos="public"` at startup if it detects the workflow is running in a public repository. This prevents agents from accumulating private-data secrecy tags in the first place — even when the compiled config grants broader access. -**Detection mechanism**: The gateway reads `GITHUB_REPOSITORY` and calls `GET /repos/{owner}/{repo}` at startup to determine repository visibility. If the repository is public and `forcePublicRepos` is `true`, the override is applied unconditionally. +**Detection mechanism**: The gateway reads `GITHUB_REPOSITORY` and calls `GET /repos/{owner}/{repo}` at startup to determine repository visibility. If the repository is public and `forcePublicRepos` is `true`, the override is applied — subject to the availability of `GITHUB_REPOSITORY`, the GitHub token, and a successful API response (see Precedence rules below). **Scope of the override**: Only the allow-only policy is affected. Guard policies, tool permissions, and other configuration are unchanged. @@ -1580,7 +1580,7 @@ The write-sink guard policy applies at the **output side**: it is checked immedi #### 10.8.2 `sink-visibility` Field -The `write-sink` object inside `guard-policies` for a safe-outputs (or equivalent) MCP server accepts the following field: +The `write-sink` object inside `guard-policies` for a safe-outputs (or equivalent) MCP server accepts the following fields: | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| @@ -1626,7 +1626,7 @@ The gh-aw compiler automatically sets `sink-visibility` as a runtime expression This runtime expression resolves to the actual repository visibility at workflow execution time (as determined by the automatic lockdown detection step). Workflow authors do not need to set `sink-visibility` manually — it is derived from the repository context automatically. -**Configuration Example** (as rendered in the generated config): +**Configuration Example** (resolved runtime value — `"public"` shown as an example after the expression is evaluated): ```json { From 8bdcee63c51523a98c1877b0c76234cc5a87bab0 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Wed, 8 Jul 2026 09:12:54 -0700 Subject: [PATCH 6/6] docs: add sinkVisibilityExemptServers, default visibility, and safety net - Add sinkVisibilityExemptServers to gateway config fields table (section 4.1.3) - Add to JSON schemas in both docs/public and pkg/workflow - Document default sink-visibility="public" for non-safe-outputs (section 10.8.6) - Document safe-outputs runtime safety net (section 10.8.7) - Expand section 10.9 to support list form of private-to-public-flows - Add compiler responsibilities for list form (validation + config emission) - Update interaction matrix with list form compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../schemas/mcp-gateway-config.schema.json | 7 ++ .../src/content/docs/reference/mcp-gateway.md | 74 ++++++++++++++++--- .../schemas/mcp-gateway-config.schema.json | 7 ++ 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/docs/public/schemas/mcp-gateway-config.schema.json b/docs/public/schemas/mcp-gateway-config.schema.json index 731a88fc3c2..806eebe0d40 100644 --- a/docs/public/schemas/mcp-gateway-config.schema.json +++ b/docs/public/schemas/mcp-gateway-config.schema.json @@ -302,6 +302,13 @@ "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.", "default": true + }, + "sinkVisibilityExemptServers": { + "type": "array", + "description": "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 MCP Gateway Specification section 10.9.", + "items": { + "type": "string" + } } }, "required": ["port", "domain", "apiKey"], diff --git a/docs/src/content/docs/reference/mcp-gateway.md b/docs/src/content/docs/reference/mcp-gateway.md index 8f51d2643ce..06ad1f234bc 100644 --- a/docs/src/content/docs/reference/mcp-gateway.md +++ b/docs/src/content/docs/reference/mcp-gateway.md @@ -255,6 +255,7 @@ The `gateway` section is required and configures gateway-specific behavior: | `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. | +| `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. | #### 4.1.3.1 Payload Directory Path Validation @@ -1646,12 +1647,40 @@ This runtime expression resolves to the actual repository visibility at workflow **Compliance Tests**: T-WS-001 through T-WS-003 (Section 11.1.12) -### 10.9 Cross-Visibility Opt-Out (`private-to-public-flows: allow`) +#### 10.8.6 Default Sink Visibility (Security-by-Default) -Workflow authors who intentionally allow private→public data flows can opt out of both cross-visibility protections (the `forcePublicRepos` override in Section 4.1.3.8 and the `sink-visibility` runtime verification in Section 10.8.4) by declaring `private-to-public-flows: allow` in workflow frontmatter. +Non-safe-outputs write-sink servers receive a default `sink-visibility="public"` when no explicit value is configured. This security-by-default behavior assumes that external sinks (e.g., Playwright, Slack, third-party services) release data publicly unless proven otherwise. + +**Rules:** +1. If a write-sink server is NOT safe-outputs (or its legacy `safeoutputs` form) and has no explicit `sink-visibility` configured → the gateway defaults to `"public"`. +2. If the server ID appears in `gateway.sinkVisibilityExemptServers` → the default is NOT applied. +3. If `sinkVisibilityExemptServers` contains `"*"` → no server receives the default. +4. If `forcePublicRepos` is `false` → all servers are implicitly exempt (blanket opt-out). + +**Rationale**: External MCP servers (Playwright, Slack, email services) typically publish outputs to locations beyond the workflow author's control. Treating them as public sinks by default prevents accidental data exfiltration through these channels. + +#### 10.8.7 Safe-Outputs Runtime Safety Net + +As a defense-in-depth measure, the gateway applies a runtime safety net specifically for safe-outputs when all of the following are true: +- The server ID is `"safe-outputs"` (or the legacy `"safeoutputs"` form) +- No `sink-visibility` is configured (compiler omitted it) +- `GITHUB_REPOSITORY` is set and a GitHub token is available +- The GitHub API confirms the workflow repository is **public** + +In this case, the gateway forces `sink-visibility="public"` on safe-outputs with a warning log. This makes the gateway self-defending even without compiler cooperation — if the compiler fails to set sink-visibility for any reason (legacy workflows, misconfiguration), the gateway still prevents exfiltration to public repos. + +**Note**: This safety net does NOT fire when `sink-visibility` is explicitly set to `"private"` or `"internal"` — the runtime verification in Section 10.8.4 handles that case instead. + +### 10.9 Cross-Visibility Opt-Out (`private-to-public-flows`) + +Workflow authors who intentionally allow private→public data flows can opt out of cross-visibility protections by declaring `private-to-public-flows` in workflow frontmatter. This field accepts two forms: + +- **`allow`** (string) — blanket opt-out that disables both `forcePublicRepos` and all sink-visibility enforcement +- **`[server1, server2, ...]`** (list) — targeted opt-out that exempts only the listed servers from default sink-visibility enforcement #### 10.9.1 Workflow Frontmatter +**Blanket opt-out:** ```yaml --- tools: @@ -1661,30 +1690,53 @@ private-to-public-flows: allow --- ``` +**Targeted opt-out (list form):** +```yaml +--- +tools: + - github + - safe-outputs + - playwright +private-to-public-flows: + - playwright +--- +``` + #### 10.9.2 Constraints -- **Incompatible with `guards_mode: strict`** — the compiler MUST reject this combination at compile time with an error. -- When paired with non-strict mode (`filter` or `propagate`), disables both: +- **Blanket `allow` is incompatible with `guards_mode: strict`** — the compiler MUST reject this combination at compile time with an error. +- **List form IS compatible with `guards_mode: strict`** — it only relaxes the default sink-visibility for named servers while keeping all other protections active. +- Server IDs in the list form MUST map to actual MCP servers declared in the workflow's `tools` list. The compiler MUST reject unknown server IDs at compile time. +- When paired with non-strict mode (`filter` or `propagate`), the blanket form disables both: - Forced `repos="public"` override (Section 4.1.3.8) - `sink-visibility` runtime verification override (Section 10.8.4) #### 10.9.3 Compiler Responsibilities -When the compiler encounters `private-to-public-flows: allow`: +When the compiler encounters `private-to-public-flows`: +**Blanket `allow`:** 1. **Validate**: If `guards_mode` is `strict`, emit a compile error. 2. **Emit config**: Set `gateway.forcePublicRepos: false` in the generated JSON stdin config. 3. **Skip sink-visibility override**: Do not set `sink-visibility: "public"` even if the target repo is public at compile time. 4. **Audit trail**: Log that the workflow author opted out of cross-visibility protection. +**List form `[server1, server2, ...]`:** +1. **Validate**: Each server ID MUST exist in the workflow's declared `tools` list. Reject unknown IDs at compile time. +2. **Emit config**: Set `gateway.sinkVisibilityExemptServers: ["server1", "server2"]` in the generated JSON stdin config. +3. **Do NOT set `forcePublicRepos: false`** — the forced repos="public" override remains active. +4. **Audit trail**: Log which servers are exempt from default sink-visibility. + #### 10.9.4 Interaction Matrix -| `guards_mode` | `private-to-public-flows` | Forced `repos=public` | `sink-visibility` enforced | -|---|---|---|---| -| `strict` | `allow` | ❌ **Rejected** (compile error) | — | -| `strict` | *(omitted)* | ✅ Yes | ✅ Yes | -| `filter` / `propagate` | `allow` | ❌ Disabled | ❌ Disabled | -| `filter` / `propagate` | *(omitted)* | ✅ Yes | ✅ Yes | +| `guards_mode` | `private-to-public-flows` | Forced `repos=public` | Default `sink-visibility` enforced | Strict-mode compatible | +|---|---|---|---|---| +| `strict` | `allow` | ❌ **Rejected** (compile error) | — | ❌ | +| `strict` | `[servers...]` | ✅ Yes | ✅ Yes (except listed servers) | ✅ | +| `strict` | *(omitted)* | ✅ Yes | ✅ Yes | ✅ | +| `filter` / `propagate` | `allow` | ❌ Disabled | ❌ Disabled | N/A | +| `filter` / `propagate` | `[servers...]` | ✅ Yes | ✅ Yes (except listed servers) | N/A | +| `filter` / `propagate` | *(omitted)* | ✅ Yes | ✅ Yes | N/A | #### 10.9.5 Security Rationale diff --git a/pkg/workflow/schemas/mcp-gateway-config.schema.json b/pkg/workflow/schemas/mcp-gateway-config.schema.json index 28ba2eac85d..463f2f702b6 100644 --- a/pkg/workflow/schemas/mcp-gateway-config.schema.json +++ b/pkg/workflow/schemas/mcp-gateway-config.schema.json @@ -266,6 +266,13 @@ "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.", "default": true + }, + "sinkVisibilityExemptServers": { + "type": "array", + "description": "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 MCP Gateway Specification section 10.9.", + "items": { + "type": "string" + } } }, "required": ["port", "domain", "apiKey"],