From 8459de6bbd7b43dc5ff1c20d97951f0140632b81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:30:05 +0000 Subject: [PATCH 1/4] Initial plan From fab300add11bdf2b6409bf0a3a5426e455a81bb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:46:57 +0000 Subject: [PATCH 2/4] Add agentId/agentIds mutual exclusion to MCP Gateway spec and schemas Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .../schemas/mcp-gateway-config.schema.json | 46 ++++- .../src/content/docs/reference/mcp-gateway.md | 157 ++++++++++++---- .../mcp_gateway_config_schema_test.go | 167 +++++++++++++++++- .../schemas/mcp-gateway-config.schema.json | 44 ++++- 4 files changed, 366 insertions(+), 48 deletions(-) diff --git a/docs/public/schemas/mcp-gateway-config.schema.json b/docs/public/schemas/mcp-gateway-config.schema.json index 54306933e86..a525ac308e4 100644 --- a/docs/public/schemas/mcp-gateway-config.schema.json +++ b/docs/public/schemas/mcp-gateway-config.schema.json @@ -246,11 +246,20 @@ ], "description": "HTTP server port for the gateway. The gateway exposes endpoints at http://{domain}:{port}/. Can be an integer (1-65535) or a variable expression like '${MCP_GATEWAY_PORT}'." }, - "apiKey": { + "agentId": { "type": "string", - "description": "API key for authentication. When configured, clients must include 'Authorization: ' header in all RPC requests (the API key is used directly without Bearer or other scheme prefix). Per MCP Gateway Specification section 7.1, the authorization header format is 'Authorization: ' where the API key is the complete header value. API keys must not be logged in plaintext per section 7.2.", + "description": "Single agent/session identifier used for authentication and DIFC session-state keying. When configured, clients must include an 'Authorization' header containing this identifier in all RPC requests. Mutually exclusive with agentIds. Per MCP Gateway Specification section 7.1, the authorization header value is the complete agent identifier. Agent identifiers must not be logged in plaintext per section 7.2.", "minLength": 1 }, + "agentIds": { + "type": "array", + "description": "List of agent/session identifiers, enabling a primary agent and enclave sessions to share one gateway instance while retaining independent DIFC state and policies. Each entry may be used as an 'Authorization' header value. Mutually exclusive with agentId. Per MCP Gateway Specification section 4.1.3.9.", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + }, "domain": { "oneOf": [ { @@ -323,7 +332,17 @@ } } }, - "required": ["port", "domain", "apiKey"], + "required": ["port", "domain"], + "oneOf": [ + { + "required": ["agentId"], + "not": { "required": ["agentIds"] } + }, + { + "required": ["agentIds"], + "not": { "required": ["agentId"] } + } + ], "additionalProperties": false }, "writeSinkGuardPolicyConfig": { @@ -404,7 +423,7 @@ "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "gateway-secret-token" + "agentId": "gateway-secret-token" } }, { @@ -442,7 +461,7 @@ "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "gateway-secret-token" + "agentId": "gateway-secret-token" } }, { @@ -466,11 +485,26 @@ "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "gateway-secret-token" + "agentId": "gateway-secret-token" }, "customSchemas": { "safeinputs": "https://docs.github.com/gh-aw/schemas/mcp-scripts-config.schema.json" } + }, + { + "mcpServers": { + "github": { + "container": "ghcr.io/github/github-mcp-server:latest", + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" + } + } + }, + "gateway": { + "port": 8080, + "domain": "localhost", + "agentIds": ["primary-agent", "enclave-agent"] + } } ] } diff --git a/docs/src/content/docs/reference/mcp-gateway.md b/docs/src/content/docs/reference/mcp-gateway.md index 731809570e6..f844fb9b0c3 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.16.0 +**Version**: 1.17.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) @@ -202,7 +202,7 @@ The gateway MUST accept configuration via stdin in JSON format conforming to the }, "gateway": { "port": 8080, - "apiKey": "string", + "agentId": "string", "domain": "string", "startupTimeout": 30, "toolTimeout": 60 @@ -244,7 +244,8 @@ The `gateway` section is required and configures gateway-specific behavior: |-------|------|----------|-------------| | `port` | integer | Yes | HTTP server port | | `domain` | string | Yes | Gateway domain (localhost or host.docker.internal) | -| `apiKey` | string | Yes | API key for authentication | +| `agentId` | string | Conditional* | Single agent/session identifier used for authentication. Mutually exclusive with `agentIds`. See Section 4.1.3.9. | +| `agentIds` | array[string] | Conditional* | List of agent/session identifiers (at least one, each non-empty), used for authentication. Mutually exclusive with `agentId`. See Section 4.1.3.9. | | `startupTimeout` | integer | No | Server startup timeout in seconds (default: 30) | | `toolTimeout` | integer | No | Tool invocation timeout in seconds (default: 60) | | `payloadDir` | string | No | Directory path for storing large payload JSON files for authenticated clients | @@ -257,6 +258,8 @@ The `gateway` section is required and configures gateway-specific behavior: | `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. | +*Exactly one of `agentId` or `agentIds` MUST be specified; the two fields are mutually exclusive. See Section 4.1.3.9. + #### 4.1.3.1 Payload Directory Path Validation When the optional `payloadDir` field is provided in the gateway configuration, it specifies a directory path where the gateway stores large payload JSON files for authenticated clients. This enables efficient handling of large response payloads by offloading them to the filesystem. @@ -330,7 +333,7 @@ payload_dir = "/tmp/jq-payloads" payload_path_prefix = "/workspace/payloads" port = 8080 domain = "localhost" -apiKey = "secret" +agent_id = "secret" ``` **Use Cases**: @@ -388,7 +391,7 @@ The optional `trustedBots` field in the gateway configuration passes an addition "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "trustedBots": [ "github-actions[bot]", "copilot-swe-agent[bot]" @@ -432,7 +435,7 @@ The optional `keepaliveInterval` field in the gateway configuration controls how "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "keepaliveInterval": 300 } } @@ -471,7 +474,7 @@ The optional `sessionTimeout` field in the gateway configuration controls how lo "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "sessionTimeout": "4h" } } @@ -516,7 +519,7 @@ The optional `opentelemetry` object in the gateway configuration enables the gat "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "opentelemetry": { "endpoint": "https://collector.example.com:4318/v1/traces", "serviceName": "my-mcp-gateway" @@ -586,7 +589,7 @@ When `forcePublicRepos` is `true` (the default), the gateway overrides the compi "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "forcePublicRepos": false } } @@ -594,6 +597,57 @@ When `forcePublicRepos` is `true` (the default), the gateway overrides the compi **Compliance Test**: T-WS-004 (Section 11.1.12) +#### 4.1.3.9 Agent Identifier Configuration (`agentId` / `agentIds`) + +The gateway configuration MUST select exactly one of the following mutually exclusive forms to configure the agent/session identifier(s) used for authentication and DIFC session-state keying: + +- `agentId` (string): a single agent/session identifier. +- `agentIds` (array of strings): one or more agent/session identifiers, enabling a primary agent and enclave sessions to share one gateway instance while retaining independent DIFC state and policies. + +**Requirements**: + +- A gateway configuration MUST specify exactly one of `agentId` or `agentIds`. Specifying both, or neither, MUST be rejected as invalid configuration. +- `agentId`, when present, MUST be a non-empty string. +- `agentIds`, when present, MUST be an array containing at least one item, and every item MUST be a non-empty string. +- When `agentIds` contains multiple entries, the gateway MUST maintain independent DIFC session-label state per matched identifier (see Section 7.2). + +**Configuration Example (singular)**: + +```json +{ + "gateway": { + "port": 8080, + "domain": "localhost", + "agentId": "primary-agent" + } +} +``` + +**Configuration Example (plural)**: + +```json +{ + "gateway": { + "port": 8080, + "domain": "localhost", + "agentIds": ["primary-agent", "enclave-agent"] + } +} +``` + +**mcpg TOML spelling**: + +```toml +[gateway] +port = 8080 +domain = "localhost" +agent_id = "primary-agent" +# OR +agent_ids = ["primary-agent", "enclave-agent"] +``` + +**Compatibility**: Implementations MAY continue to accept the legacy `api_key`/`apiKey` spelling internally for backward compatibility with existing deployments, but `apiKey` MUST NOT appear in generated or published configuration; `agentId` or `agentIds` MUST be used instead. Runtime mapping from individual identifiers to allow-only policies is out of scope for this section and is tracked as follow-up work. + #### 4.1.3a Top-Level Configuration Fields The following fields MAY be specified at the top level of the configuration: @@ -620,7 +674,7 @@ Custom server types MUST be registered in the `customSchemas` field at the top l "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "secret" + "agentId": "secret" }, "customSchemas": { "safeinputs": "https://docs.github.com/gh-aw/schemas/mcp-scripts-config.schema.json" @@ -657,7 +711,7 @@ When a server configuration includes a `type` field with a value not in `["stdio "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "secret" + "agentId": "secret" }, "customSchemas": { "safeinputs": "https://docs.github.com/gh-aw/schemas/mcp-scripts-config.schema.json" @@ -848,7 +902,7 @@ POST /close ```http POST /mcp/{server-name} HTTP/1.1 Content-Type: application/json -Authorization: +Authorization: { "jsonrpc": "2.0", @@ -898,7 +952,7 @@ The gateway MUST provide a `/close` endpoint for graceful shutdown and resource ```http POST /close HTTP/1.1 -Authorization: +Authorization: ``` **Note**: The format of the `Authorization` header is implementation-dependent. Consult your gateway implementation's documentation for the expected format. @@ -954,7 +1008,7 @@ The `/close` endpoint MUST be idempotent: **Authentication**: -The `/close` endpoint MUST require authentication when `gateway.apiKey` is configured. Requests without valid authentication MUST be rejected with HTTP 401 Unauthorized. +The `/close` endpoint MUST require authentication when `gateway.agentId` or `gateway.agentIds` is configured. Requests without valid authentication MUST be rejected with HTTP 401 Unauthorized. #### 5.1.4 Request Routing @@ -1050,7 +1104,7 @@ After successful initialization, the gateway MUST: "type": "http", "url": "http://{domain}:{port}/mcp/server-name", "headers": { - "Authorization": "{apiKey}" + "Authorization": "{agentId}" }, "tools": ["*"] } @@ -1126,46 +1180,47 @@ The gateway MUST NOT: ### 7.1 Authorization Header Format -The MCP Gateway uses a simple API key authentication scheme. When `gateway.apiKey` is configured: +The MCP Gateway uses a simple agent identifier authentication scheme. When `gateway.agentId` or `gateway.agentIds` is configured: -- The `Authorization` header contains the API key value -- Implementations MAY use different formats (e.g., direct value or Bearer scheme) +- The `Authorization` header contains an agent/session identifier value (the configured `agentId`, or one of the configured `agentIds` entries) +- Implementations MAY use different formats (e.g., direct value or a scheme-prefixed value) - The specific format is implementation-dependent > [!WARNING] -> The gateway API key should not be treated as a secure lock against code already running inside the agent container. A sufficiently capable agent may extract it from in-memory process state or other runtime channels. Treat this key as leaked by design and rely on container isolation, network controls, and staged permission boundaries for defense in depth. +> The gateway agent identifier should not be treated as a secure lock against code already running inside the agent container. A sufficiently capable agent may extract it from in-memory process state or other runtime channels. Treat this identifier as leaked by design and rely on container isolation, network controls, and staged permission boundaries for defense in depth. **Example formats**: ```http -Authorization: my-secret-api-key-12345 +Authorization: my-secret-agent-id-12345 ``` or ```http -Authorization: Bearer my-secret-api-key-12345 +Authorization: ****** ``` This authentication scheme provides flexibility for different implementation requirements. -### 7.2 API Key Authentication +### 7.2 Agent Identifier Authentication -When `gateway.apiKey` is configured, the gateway MUST: +When `gateway.agentId` or `gateway.agentIds` is configured, the gateway MUST: 1. Require `Authorization` header on all RPC requests to `/mcp/{server-name}` and `/close` endpoints - The specific format of the Authorization header is implementation-dependent - Implementations SHOULD document their expected format + - When `agentIds` is configured, the gateway MUST accept an `Authorization` header value matching any configured entry, and MUST key DIFC session state independently per matched identifier 2. Reject requests with missing or invalid tokens (HTTP 401) 3. Reject requests with malformed Authorization headers (HTTP 400) -4. NOT log API keys in plaintext +4. NOT log agent identifiers in plaintext -### 7.3 Optimal Temporary API Key +### 7.3 Optimal Temporary Agent Identifier -The gateway SHOULD support temporary API keys: +The gateway SHOULD support temporary agent identifiers: -1. Generate a random API key on startup if not provided -2. Include key in stdout configuration output +1. Generate a random agent identifier on startup if not provided +2. Include the identifier in stdout configuration output ### 7.4 Authentication Exemptions @@ -1927,7 +1982,7 @@ Implementations SHOULD provide: "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "gateway-secret-token" + "agentId": "gateway-secret-token" } } ``` @@ -1951,7 +2006,7 @@ Implementations SHOULD provide: "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "gateway-secret-token" + "agentId": "gateway-secret-token" } } ``` @@ -2035,7 +2090,7 @@ The `registry` field documents the MCP server's installation location in an MCP "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "gateway-secret-token" + "agentId": "gateway-secret-token" } } ``` @@ -2063,7 +2118,7 @@ The following example configures the gateway to export traces to an OTLP collect "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "opentelemetry": { "endpoint": "https://collector.example.com:4318/v1/traces", "serviceName": "my-workflow-gateway" @@ -2091,7 +2146,7 @@ The following example propagates an existing distributed trace into the gateway, "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "opentelemetry": { "endpoint": "https://collector.example.com:4318/v1/traces", "traceId": "${PARENT_TRACE_ID}", @@ -2104,6 +2159,28 @@ The following example propagates an existing distributed trace into the gateway, `PARENT_TRACE_ID` and `PARENT_SPAN_ID` are 32-character and 16-character lowercase hex strings respectively, typically injected as environment variables by the orchestrating system. +#### A.8 Gateway with Multiple Agent Identifiers (`agentIds`) + +The following example configures a gateway shared by a primary agent and an enclave agent, each with its own identifier and independently keyed DIFC session state: + +```json +{ + "mcpServers": { + "github": { + "container": "ghcr.io/github/github-mcp-server:latest", + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" + } + } + }, + "gateway": { + "port": 8080, + "domain": "localhost", + "agentIds": ["primary-agent", "enclave-agent"] + } +} +``` + ### Appendix B: Gateway Lifecycle Examples #### B.1 Closing the Gateway @@ -2181,7 +2258,7 @@ Content-Type: application/json #### D.1 Credential Handling -- API keys MUST NOT be logged +- Agent identifiers (`agentId`/`agentIds`) MUST NOT be logged - Environment variables MUST be isolated per server - Secrets SHOULD be cleared from memory after use @@ -2231,6 +2308,18 @@ Content-Type: application/json ## Change Log +### Version 1.17.0 (Draft) + +- **BREAKING**: Removed `gateway.apiKey` from the specification and published schemas (Sections 4.1.1, 4.1.3, 7.1, 7.2) + - The field's terminology was misleading: it represented agent/session identity, not a separately modeled API credential + - Replaced by `gateway.agentId` (singular) and `gateway.agentIds` (plural), which are mutually exclusive +- **Added**: `gateway.agentId` — a single agent/session identifier, equivalent in behavior to the former `apiKey` field (Section 4.1.3.9) +- **Added**: `gateway.agentIds` — an array of agent/session identifiers (at least one, each a non-empty string), enabling a primary agent and enclave sessions to share one gateway instance while retaining independent DIFC state and policies (Section 4.1.3.9) +- **Added**: Section 4.1.3.9 — Agent Identifier Configuration (`agentId` / `agentIds`), documenting the mutually exclusive singular/plural forms and the corresponding mcpg TOML spelling `agent_id` / `agent_ids` +- **Changed**: A gateway configuration MUST select exactly one of `agentId` or `agentIds` (Sections 4.1.3, 4.1.3.9) +- **Updated**: JSON Schema — `gatewayConfig` definition now requires `port`, `domain`, and exactly one of `agentId`/`agentIds`; `apiKey` removed from both canonical schema copies +- **Note**: Implementations MAY continue to accept the legacy `api_key`/`apiKey` spelling internally for backward compatibility, but it is no longer part of the normative or published configuration contract + ### Version 1.16.0 (Draft) - **Changed**: `opentelemetry.endpoint` now accepts HTTP or HTTPS OTLP/HTTP collector URLs (Section 4.1.3.7) diff --git a/pkg/workflow/mcp_gateway_config_schema_test.go b/pkg/workflow/mcp_gateway_config_schema_test.go index 0e5321a7b99..bd04acacca1 100644 --- a/pkg/workflow/mcp_gateway_config_schema_test.go +++ b/pkg/workflow/mcp_gateway_config_schema_test.go @@ -4,6 +4,7 @@ package workflow import ( "encoding/json" + "maps" "os" "regexp" "testing" @@ -48,9 +49,9 @@ func TestMCPGatewayConfigSchemaAcceptsHTTPOTLPEndpoint(t *testing.T) { config := map[string]any{ "mcpServers": map[string]any{}, "gateway": map[string]any{ - "port": 8080, - "domain": "localhost", - "apiKey": "test-key", + "port": 8080, + "domain": "localhost", + "agentId": "test-agent", "opentelemetry": map[string]any{ "endpoint": "http://127.0.0.1:4318/v1/traces", }, @@ -66,6 +67,166 @@ func TestMCPGatewayConfigSchemaAcceptsHTTPOTLPEndpoint(t *testing.T) { } } +func TestMCPGatewayConfigSchemaAgentIdentifiers(t *testing.T) { + // The docs/public copy is intentionally excluded here: it additionally defines + // customServerConfig with a negative-lookahead regex pattern that the schema + // compiler's regex engine does not support (pre-existing, unrelated to + // agentId/agentIds). Both copies still enforce the same agentId/agentIds + // constraints — see the package schema below. + schemaPaths := []string{ + "schemas/mcp-gateway-config.schema.json", + } + + baseConfig := func() map[string]any { + return map[string]any{ + "mcpServers": map[string]any{}, + "gateway": map[string]any{ + "port": 8080, + "domain": "localhost", + }, + } + } + + tests := []struct { + name string + gateway map[string]any + wantErr bool + }{ + { + name: "valid single agentId", + gateway: map[string]any{"agentId": "primary-agent"}, + wantErr: false, + }, + { + name: "valid single-entry agentIds", + gateway: map[string]any{"agentIds": []any{"primary-agent"}}, + wantErr: false, + }, + { + name: "valid multi-entry agentIds", + gateway: map[string]any{"agentIds": []any{"primary-agent", "enclave-agent"}}, + wantErr: false, + }, + { + name: "empty agentIds array", + gateway: map[string]any{"agentIds": []any{}}, + wantErr: true, + }, + { + name: "empty string entry in agentIds", + gateway: map[string]any{"agentIds": []any{""}}, + wantErr: true, + }, + { + name: "empty agentId string", + gateway: map[string]any{"agentId": ""}, + wantErr: true, + }, + { + name: "both agentId and agentIds", + gateway: map[string]any{"agentId": "primary-agent", "agentIds": []any{"enclave-agent"}}, + wantErr: true, + }, + { + name: "neither agentId nor agentIds", + gateway: map[string]any{}, + wantErr: true, + }, + { + name: "removed apiKey field", + gateway: map[string]any{"apiKey": "gateway-secret-token"}, + wantErr: true, + }, + } + + for _, schemaPath := range schemaPaths { + t.Run(schemaPath, func(t *testing.T) { + schemaJSON, err := os.ReadFile(schemaPath) + if err != nil { + t.Fatalf("failed to read schema: %v", err) + } + + schema, err := compileSchema(string(schemaJSON), "https://docs.github.com/gh-aw/schemas/mcp-gateway-config.schema.json") + if err != nil { + t.Fatalf("failed to compile schema: %v", err) + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + config := baseConfig() + gateway := config["gateway"].(map[string]any) + maps.Copy(gateway, tc.gateway) + + err := schema.Validate(config) + if tc.wantErr && err == nil { + t.Fatalf("expected validation error for gateway config %+v, got none", tc.gateway) + } + if !tc.wantErr && err != nil { + t.Fatalf("expected gateway config %+v to validate, got error: %v", tc.gateway, err) + } + }) + } + }) + } +} + +// TestMCPGatewayConfigSchemaCopiesStayInSync verifies that every published +// mcp-gateway-config.schema.json copy defines agentId/agentIds consistently +// and no longer defines apiKey, even for copies (like docs/public) that embed +// additional definitions the schema compiler's regex engine cannot compile. +func TestMCPGatewayConfigSchemaCopiesStayInSync(t *testing.T) { + schemaPaths := []string{ + "schemas/mcp-gateway-config.schema.json", + "../../docs/public/schemas/mcp-gateway-config.schema.json", + } + + for _, schemaPath := range schemaPaths { + t.Run(schemaPath, func(t *testing.T) { + schemaJSON, err := os.ReadFile(schemaPath) + if err != nil { + t.Fatalf("failed to read schema: %v", err) + } + + var schema map[string]any + if err := json.Unmarshal(schemaJSON, &schema); err != nil { + t.Fatalf("failed to parse schema: %v", err) + } + + definitions := requireObject(t, schema, "definitions") + gatewayConfig := requireObject(t, definitions, "gatewayConfig") + properties := requireObject(t, gatewayConfig, "properties") + + if _, hasAPIKey := properties["apiKey"]; hasAPIKey { + t.Fatal("expected gatewayConfig.properties to no longer define apiKey") + } + if _, hasAgentID := properties["agentId"]; !hasAgentID { + t.Fatal("expected gatewayConfig.properties to define agentId") + } + if _, hasAgentIDs := properties["agentIds"]; !hasAgentIDs { + t.Fatal("expected gatewayConfig.properties to define agentIds") + } + + required, ok := gatewayConfig["required"].([]any) + if !ok { + t.Fatal("expected gatewayConfig.required to be an array") + } + for _, r := range required { + if r == "apiKey" { + t.Fatal("expected gatewayConfig.required to no longer require apiKey") + } + if r == "agentId" || r == "agentIds" { + t.Fatalf("expected agentId/agentIds to be enforced via oneOf, not required directly, got required entry %q", r) + } + } + + oneOf, ok := gatewayConfig["oneOf"].([]any) + if !ok || len(oneOf) != 2 { + t.Fatalf("expected gatewayConfig.oneOf to encode agentId/agentIds mutual exclusion, got %v", gatewayConfig["oneOf"]) + } + }) + } +} + func mcpGatewayOTLPEndpointPattern(t *testing.T, schemaPath string) string { t.Helper() diff --git a/pkg/workflow/schemas/mcp-gateway-config.schema.json b/pkg/workflow/schemas/mcp-gateway-config.schema.json index c960ce07ffd..e8ada032721 100644 --- a/pkg/workflow/schemas/mcp-gateway-config.schema.json +++ b/pkg/workflow/schemas/mcp-gateway-config.schema.json @@ -220,11 +220,20 @@ ], "description": "HTTP server port for the gateway. The gateway exposes endpoints at http://{domain}:{port}/. Can be an integer (1-65535) or a variable expression like '${MCP_GATEWAY_PORT}'." }, - "apiKey": { + "agentId": { "type": "string", - "description": "API key for authentication. When configured, clients must include 'Authorization: ' header in all RPC requests (the API key is used directly without Bearer or other scheme prefix). Per MCP Gateway Specification section 7.1, the authorization header format is 'Authorization: ' where the API key is the complete header value. API keys must not be logged in plaintext per section 7.2.", + "description": "Single agent/session identifier used for authentication and DIFC session-state keying. When configured, clients must include an 'Authorization' header containing this identifier in all RPC requests. Mutually exclusive with agentIds. Per MCP Gateway Specification section 7.1, the authorization header value is the complete agent identifier. Agent identifiers must not be logged in plaintext per section 7.2.", "minLength": 1 }, + "agentIds": { + "type": "array", + "description": "List of agent/session identifiers, enabling a primary agent and enclave sessions to share one gateway instance while retaining independent DIFC state and policies. Each entry may be used as an 'Authorization' header value. Mutually exclusive with agentId. Per MCP Gateway Specification section 4.1.3.9.", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + }, "domain": { "oneOf": [ { @@ -297,7 +306,17 @@ } } }, - "required": ["port", "domain", "apiKey"], + "required": ["port", "domain"], + "oneOf": [ + { + "required": ["agentId"], + "not": { "required": ["agentIds"] } + }, + { + "required": ["agentIds"], + "not": { "required": ["agentId"] } + } + ], "additionalProperties": false }, "writeSinkGuardPolicyConfig": { @@ -378,7 +397,7 @@ "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "gateway-secret-token" + "agentId": "gateway-secret-token" } }, { @@ -416,7 +435,22 @@ "gateway": { "port": 8080, "domain": "localhost", - "apiKey": "gateway-secret-token" + "agentId": "gateway-secret-token" + } + }, + { + "mcpServers": { + "github": { + "container": "ghcr.io/github/github-mcp-server:latest", + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" + } + } + }, + "gateway": { + "port": 8080, + "domain": "localhost", + "agentIds": ["primary-agent", "enclave-agent"] } } ] From 65be436c7ce9f1c4ca7c96f0cfe436a2a0f72373 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:13:55 +0000 Subject: [PATCH 3/4] Address remaining review feedback on agentId/agentIds spec Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../schemas/mcp-gateway-config.schema.json | 2 +- .../src/content/docs/reference/mcp-gateway.md | 13 ++-- .../mcp_gateway_config_schema_test.go | 63 +++++++++++++++++-- .../schemas/mcp-gateway-config.schema.json | 2 +- 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/docs/public/schemas/mcp-gateway-config.schema.json b/docs/public/schemas/mcp-gateway-config.schema.json index a525ac308e4..d87ebd2cafd 100644 --- a/docs/public/schemas/mcp-gateway-config.schema.json +++ b/docs/public/schemas/mcp-gateway-config.schema.json @@ -248,7 +248,7 @@ }, "agentId": { "type": "string", - "description": "Single agent/session identifier used for authentication and DIFC session-state keying. When configured, clients must include an 'Authorization' header containing this identifier in all RPC requests. Mutually exclusive with agentIds. Per MCP Gateway Specification section 7.1, the authorization header value is the complete agent identifier. Agent identifiers must not be logged in plaintext per section 7.2.", + "description": "Single agent/session identifier used for authentication and DIFC session-state keying. When configured, clients must include an 'Authorization' header conveying this identifier in all RPC requests; the exact header format (e.g. a bare value or a scheme-prefixed value) is implementation-dependent per MCP Gateway Specification section 7.1. Mutually exclusive with agentIds. Agent identifiers must not be logged in plaintext per section 7.2.", "minLength": 1 }, "agentIds": { diff --git a/docs/src/content/docs/reference/mcp-gateway.md b/docs/src/content/docs/reference/mcp-gateway.md index f844fb9b0c3..6aa014a53a0 100644 --- a/docs/src/content/docs/reference/mcp-gateway.md +++ b/docs/src/content/docs/reference/mcp-gateway.md @@ -609,7 +609,7 @@ The gateway configuration MUST select exactly one of the following mutually excl - A gateway configuration MUST specify exactly one of `agentId` or `agentIds`. Specifying both, or neither, MUST be rejected as invalid configuration. - `agentId`, when present, MUST be a non-empty string. - `agentIds`, when present, MUST be an array containing at least one item, and every item MUST be a non-empty string. -- When `agentIds` contains multiple entries, the gateway MUST maintain independent DIFC session-label state per matched identifier (see Section 7.2). +- `agentIds` enables associating multiple agent/session identifiers with one gateway instance in configuration. Runtime authentication and per-identifier DIFC session-state keying are out of scope for this section and are tracked as follow-up work (see Section 7.2). **Configuration Example (singular)**: @@ -1198,7 +1198,7 @@ Authorization: my-secret-agent-id-12345 or ```http -Authorization: ****** +Authorization: Token my-other-agent-id-67890 ``` This authentication scheme provides flexibility for different implementation requirements. @@ -1210,17 +1210,16 @@ When `gateway.agentId` or `gateway.agentIds` is configured, the gateway MUST: 1. Require `Authorization` header on all RPC requests to `/mcp/{server-name}` and `/close` endpoints - The specific format of the Authorization header is implementation-dependent - Implementations SHOULD document their expected format - - When `agentIds` is configured, the gateway MUST accept an `Authorization` header value matching any configured entry, and MUST key DIFC session state independently per matched identifier 2. Reject requests with missing or invalid tokens (HTTP 401) 3. Reject requests with malformed Authorization headers (HTTP 400) 4. NOT log agent identifiers in plaintext -### 7.3 Optimal Temporary Agent Identifier +> [!NOTE] +> This section covers configuration parsing only. Runtime authentication and routing of individual `agentIds` entries to independent DIFC session state is out of scope for this specification and is tracked as follow-up implementation work. -The gateway SHOULD support temporary agent identifiers: +### 7.3 Temporary Agent Identifiers -1. Generate a random agent identifier on startup if not provided -2. Include the identifier in stdout configuration output +A gateway configuration MUST always specify exactly one of `agentId` or `agentIds` (see Section 4.1.3.9); the gateway MUST NOT silently generate an identifier when neither is provided. Callers that need a short-lived identity (for example, an ephemeral session) SHOULD generate their own temporary identifier and supply it as `agentId` before starting the gateway. ### 7.4 Authentication Exemptions diff --git a/pkg/workflow/mcp_gateway_config_schema_test.go b/pkg/workflow/mcp_gateway_config_schema_test.go index bd04acacca1..52bc64f24bc 100644 --- a/pkg/workflow/mcp_gateway_config_schema_test.go +++ b/pkg/workflow/mcp_gateway_config_schema_test.go @@ -171,15 +171,27 @@ func TestMCPGatewayConfigSchemaAgentIdentifiers(t *testing.T) { } // TestMCPGatewayConfigSchemaCopiesStayInSync verifies that every published -// mcp-gateway-config.schema.json copy defines agentId/agentIds consistently -// and no longer defines apiKey, even for copies (like docs/public) that embed -// additional definitions the schema compiler's regex engine cannot compile. +// mcp-gateway-config.schema.json copy defines agentId/agentIds identically +// (same minLength/minItems/item type constraints) and enforces the same +// required/oneOf mutual-exclusion structure, and no longer defines apiKey, +// even for copies (like docs/public) that embed additional definitions the +// schema compiler's regex engine cannot compile. func TestMCPGatewayConfigSchemaCopiesStayInSync(t *testing.T) { schemaPaths := []string{ "schemas/mcp-gateway-config.schema.json", "../../docs/public/schemas/mcp-gateway-config.schema.json", } + type agentIdentityConstraints struct { + AgentID any `json:"agentId"` + AgentIDs any `json:"agentIds"` + Required any `json:"required"` + OneOf any `json:"oneOf"` + } + + var reference *agentIdentityConstraints + var referencePath string + for _, schemaPath := range schemaPaths { t.Run(schemaPath, func(t *testing.T) { schemaJSON, err := os.ReadFile(schemaPath) @@ -199,10 +211,12 @@ func TestMCPGatewayConfigSchemaCopiesStayInSync(t *testing.T) { if _, hasAPIKey := properties["apiKey"]; hasAPIKey { t.Fatal("expected gatewayConfig.properties to no longer define apiKey") } - if _, hasAgentID := properties["agentId"]; !hasAgentID { + agentID, hasAgentID := properties["agentId"] + if !hasAgentID { t.Fatal("expected gatewayConfig.properties to define agentId") } - if _, hasAgentIDs := properties["agentIds"]; !hasAgentIDs { + agentIDs, hasAgentIDs := properties["agentIds"] + if !hasAgentIDs { t.Fatal("expected gatewayConfig.properties to define agentIds") } @@ -223,6 +237,45 @@ func TestMCPGatewayConfigSchemaCopiesStayInSync(t *testing.T) { if !ok || len(oneOf) != 2 { t.Fatalf("expected gatewayConfig.oneOf to encode agentId/agentIds mutual exclusion, got %v", gatewayConfig["oneOf"]) } + + // Canonicalize via JSON round-trip so map key ordering doesn't + // cause spurious mismatches between the two schema copies. + canonicalize := func(v any) any { + b, err := json.Marshal(v) + if err != nil { + t.Fatalf("failed to marshal constraint for comparison: %v", err) + } + var out any + if err := json.Unmarshal(b, &out); err != nil { + t.Fatalf("failed to unmarshal constraint for comparison: %v", err) + } + return out + } + + constraints := &agentIdentityConstraints{ + AgentID: canonicalize(agentID), + AgentIDs: canonicalize(agentIDs), + Required: canonicalize(required), + OneOf: canonicalize(oneOf), + } + + if reference == nil { + reference = constraints + referencePath = schemaPath + return + } + + referenceJSON, err := json.Marshal(reference) + if err != nil { + t.Fatalf("failed to marshal reference constraints: %v", err) + } + currentJSON, err := json.Marshal(constraints) + if err != nil { + t.Fatalf("failed to marshal current constraints: %v", err) + } + if string(referenceJSON) != string(currentJSON) { + t.Fatalf("agentId/agentIds constraints in %q do not match %q:\n%s\nvs\n%s", schemaPath, referencePath, currentJSON, referenceJSON) + } }) } } diff --git a/pkg/workflow/schemas/mcp-gateway-config.schema.json b/pkg/workflow/schemas/mcp-gateway-config.schema.json index e8ada032721..2d292dc5814 100644 --- a/pkg/workflow/schemas/mcp-gateway-config.schema.json +++ b/pkg/workflow/schemas/mcp-gateway-config.schema.json @@ -222,7 +222,7 @@ }, "agentId": { "type": "string", - "description": "Single agent/session identifier used for authentication and DIFC session-state keying. When configured, clients must include an 'Authorization' header containing this identifier in all RPC requests. Mutually exclusive with agentIds. Per MCP Gateway Specification section 7.1, the authorization header value is the complete agent identifier. Agent identifiers must not be logged in plaintext per section 7.2.", + "description": "Single agent/session identifier used for authentication and DIFC session-state keying. When configured, clients must include an 'Authorization' header conveying this identifier in all RPC requests; the exact header format (e.g. a bare value or a scheme-prefixed value) is implementation-dependent per MCP Gateway Specification section 7.1. Mutually exclusive with agentIds. Agent identifiers must not be logged in plaintext per section 7.2.", "minLength": 1 }, "agentIds": { From 523d88ac286b507543704bf403dfee4c3c8ef176 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:15:09 +0000 Subject: [PATCH 4/4] Clarify diff labels in schema sync test failure message Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/mcp_gateway_config_schema_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/workflow/mcp_gateway_config_schema_test.go b/pkg/workflow/mcp_gateway_config_schema_test.go index 52bc64f24bc..c38dc7f0129 100644 --- a/pkg/workflow/mcp_gateway_config_schema_test.go +++ b/pkg/workflow/mcp_gateway_config_schema_test.go @@ -274,7 +274,7 @@ func TestMCPGatewayConfigSchemaCopiesStayInSync(t *testing.T) { t.Fatalf("failed to marshal current constraints: %v", err) } if string(referenceJSON) != string(currentJSON) { - t.Fatalf("agentId/agentIds constraints in %q do not match %q:\n%s\nvs\n%s", schemaPath, referencePath, currentJSON, referenceJSON) + t.Fatalf("agentId/agentIds constraints in %q do not match %q:\nreference (%s):\n%s\ncurrent (%s):\n%s", schemaPath, referencePath, referencePath, referenceJSON, schemaPath, currentJSON) } }) }