Skip to content

Commit 34ed844

Browse files
authored
Add mutually exclusive plural gateway agent ID configuration (#12116)
Adds plural gateway session identifiers for concurrent primary/enclave configuration while retaining singular agent IDs. Singular and plural forms are mutually exclusive. - **Configuration** - Adds `GatewayConfig.AgentIDs` (`agent_ids`) and stdin JSON `gateway.agentIds`. - Preserves plural IDs during stdin-to-internal conversion. - Rejects empty lists, blank entries, and singular/plural or legacy-alias/plural combinations. - **Published contract** - Updates the bundled schema to require exactly one of `agentId` or `agentIds`. - Adds `agentIds` array constraints: `minItems: 1`, item `minLength: 1`. - Removes `apiKey` from the published schema and configuration documentation. - **Compatibility and fixtures** - Keeps internal legacy `api_key` / `apiKey` parsing support. - Migrates schema-validated examples and test fixtures to `agentId`. ```toml [gateway] agent_ids = ["primary-agent", "enclave-agent"] ``` ```json { "gateway": { "agentIds": ["primary-agent", "enclave-agent"] } } ``` <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #12115
2 parents 146de0b + 8a8df0c commit 34ed844

41 files changed

Lines changed: 504 additions & 146 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ This gateway is used with [GitHub Agentic Workflows](https://github.com/github/g
3131
}
3232
```
3333

34-
Note: for JSON stdin configuration the `gateway` section is **required**, and it must include `port`, `domain`, and one of `agentId` / `apiKey` (`apiKey` is a deprecated alias for `agentId`). The values shown above are examples; omitting `gateway` — or any of these required fields — fails schema validation at startup. All other `gateway` fields are optional and are validated when present. Optional server fields include `entrypoint`, `entrypointArgs`, `mounts`, `args`, `connectTimeout`, and `toolTimeout`; see [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for the complete server-field reference.
34+
Note: for JSON stdin configuration the `gateway` section is **required**, and it must include `port`, `domain`, and exactly one of `agentId` or `agentIds`. The values shown above are examples; omitting `gateway` — or any of these required fields — fails schema validation at startup. All other `gateway` fields are optional and are validated when present. Optional server fields include `entrypoint`, `entrypointArgs`, `mounts`, `args`, `connectTimeout`, and `toolTimeout`; see [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for the complete server-field reference.
3535

3636
Looking for complete examples? See [`config.example.toml`](config.example.toml), [`config.example-payload-threshold.toml`](config.example-payload-threshold.toml), and [`example-http-config.json`](example-http-config.json).
3737

@@ -223,7 +223,7 @@ Key configuration fields (gateway-level under `[gateway]` in TOML / `"gateway"`
223223
| Field | Description |
224224
|-------|-------------|
225225
| `agent_id` / `agentId` | Agent/session identifier used for routing and optional auth matching |
226-
| `api_key` / `apiKey` | Deprecated alias for `agent_id` / `agentId` (accepted with warnings) |
226+
| `agent_ids` / `agentIds` | Agent/session identifiers for concurrent session isolation; configure exactly one of this field or `agent_id` / `agentId`. Each configured identifier authenticates independently, so concurrent primary/enclave sessions can each connect with their own credential. |
227227
| `port` | Metadata only; validated (1–65535) but does not control the listen address. Use the `--listen` flag to set the listen address. `MCP_GATEWAY_PORT` is read by `--validate-env` only as a required-variable presence check; port-mapping and listen-address construction are handled by wrapper scripts (`run.sh`, `run_containerized.sh`). |
228228
| `domain` | Gateway domain for external access (for example `"localhost"` in TOML/JSON, or `"${MCP_GATEWAY_DOMAIN}"` in JSON stdin where `${...}` expansion is supported) |
229229
| `startup_timeout` / `startupTimeout` | Seconds to wait for backend server startup (default `30`) |

config.example.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
# This field is stored for metadata purposes only. Valid range: 1-65535
1616
port = 3000
1717

18-
# Agent ID for routing and optional authentication matching (optional)
19-
# When set, clients must provide this agent ID in the Authorization header
20-
# Format: Authorization: <agent_id>
21-
agent_id = ""
18+
# Agent IDs for concurrent session isolation and optional authentication matching (optional)
19+
# Configure either agent_id or agent_ids, but not both.
20+
# Clients must provide a configured agent ID in the Authorization header.
21+
agent_ids = ["primary-agent", "enclave-agent"]
2222

2323
# Domain name for the gateway (optional)
2424
# Used for CORS and other domain-specific features

containers/serena-mcp-server/test-startup-time.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ CONFIG_JSON=$(cat <<EOF
6363
"gateway": {
6464
"port": ${MCP_GATEWAY_PORT},
6565
"domain": "${MCP_GATEWAY_DOMAIN}",
66-
"apiKey": "${MCP_GATEWAY_API_KEY}"
66+
"agentId": "${MCP_GATEWAY_API_KEY}"
6767
}
6868
}
6969
EOF

docs/CONFIGURATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ The `customSchemas` top-level field allows you to define custom server types bey
592592
- **TOML format**:
593593
- Uses `command` and `args` fields directly (e.g., `command = "docker"`)
594594
- Variable expansion with `${VAR_NAME}` is only supported in `[gateway.opentelemetry]` fields (the legacy `[gateway.tracing]` section does **not** support variable expansion)
595-
- Server `env` values, `url`, `args`, `gateway.agent_id`, and other non-tracing fields are not expanded
595+
- Server `env` values, `url`, `args`, `gateway.agent_id` / `gateway.agent_ids`, and other non-tracing fields are not expanded
596596
- For host environment passthrough to container `env`, use an empty string `""` value
597597
- **Common rules** (both formats):
598598
- Empty/"local" type automatically normalized to "stdio"
@@ -609,7 +609,7 @@ The `customSchemas` top-level field allows you to define custom server types bey
609609
|-------|-------------|---------|
610610
| `port` | Validated and stored for metadata purposes only. The actual listen address is always set by the `--listen` CLI flag (default `127.0.0.1:3000`). **Note:** When using JSON stdin format, `gateway.port` is required by schema validation even though TOML configs have an internal default of `3000`. | `3000` (informational only; required in JSON stdin) |
611611
| `agentId` | Agent/session identifier used for routing and optional auth matching | (disabled) |
612-
| `apiKey` | Deprecated alias for `agentId` (accepted for backward compatibility) | (deprecated) |
612+
| `agentIds` (JSON stdin) / `agent_ids` (TOML) | Agent/session identifiers for concurrent session isolation. Configure exactly one of this field or `agentId` / `agent_id`; it must contain at least one non-blank string. Each configured identifier authenticates independently against the gateway (e.g. primary/enclave), enabling concurrent sessions that don't share a single credential. | (disabled) |
613613
| `domain` | Gateway domain (`"localhost"`, `"host.docker.internal"`, or `"${VAR}"`) | (unset) |
614614
| `startupTimeout` | Seconds to wait for backend startup | `30` |
615615
| `toolTimeout` | Maximum seconds for a single tool call, enforced as a context deadline on all backend requests (stdio and HTTP) | `60` |

guards/github-guard/scripts/run_copilot_test.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ if [ "$MODE" = "yolo" ] || [ "$MODE" = "lockdown" ]; then
407407
"gateway": {
408408
"port": $GATEWAY_PORT,
409409
"domain": "localhost",
410-
"apiKey": "$GATEWAY_API_KEY"
410+
"agentId": "$GATEWAY_API_KEY"
411411
}
412412
}
413413
EOF
@@ -436,7 +436,7 @@ else
436436
"gateway": {
437437
"port": $GATEWAY_PORT,
438438
"domain": "localhost",
439-
"apiKey": "$GATEWAY_API_KEY"
439+
"agentId": "$GATEWAY_API_KEY"
440440
}
441441
}
442442
EOF

guards/github-guard/scripts/run_integration_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ echo ""
201201
"gateway": {
202202
"port": $GATEWAY_PORT,
203203
"domain": "localhost",
204-
"apiKey": "$GATEWAY_API_KEY"
204+
"agentId": "$GATEWAY_API_KEY"
205205
}
206206
}
207207
EOF

internal/cmd/output.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,14 @@ func writeGatewayConfig(cfg *config.Config, listenAddr, mode string, tlsEnabled
4646

4747
debugLog.Printf("Resolved gateway address: host=%s, port=%s", host, port)
4848

49-
// Extract agent ID from gateway config (per spec section 7.1)
50-
agentID := cfg.GetAgentID()
49+
// Extract agent ID from gateway config (per spec section 7.1). When multiple
50+
// identities are configured via gateway.agentIds, the first is used for this
51+
// downstream-facing config; every configured identity is independently valid
52+
// for authenticating directly against the gateway.
53+
var agentID string
54+
if ids := cfg.GetAgentIDs(); len(ids) > 0 {
55+
agentID = ids[0]
56+
}
5157
debugLog.Printf("Gateway config: auth_enabled=%v", agentID != "")
5258

5359
debugLog.Printf("Gateway auth: agentIDConfigured=%v", agentID != "")

internal/cmd/root.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ func run(cmd *cobra.Command, args []string) error {
288288
// Per spec §7.3: generate a random agent identifier on startup if none is configured.
289289
// The generated value is set in the config so it propagates to both the HTTP
290290
// server authentication and the stdout configuration output (spec §5.4).
291-
if cfg.GetAgentID() == "" {
291+
// gateway.agentIds (plural) counts as configured here, so a random ID is only
292+
// generated when neither the singular nor plural form is set.
293+
if len(cfg.GetAgentIDs()) == 0 {
292294
randomKey, err := auth.GenerateRandomAgentID()
293295
if err != nil {
294296
return fmt.Errorf("failed to generate random agent ID: %w", err)
@@ -363,7 +365,7 @@ func run(cmd *cobra.Command, args []string) error {
363365
}()
364366

365367
// Create HTTP server based on mode, wire BaseContext and shutdown hooks.
366-
httpServer := buildHTTPServer(ctx, mode, listenAddr, unifiedServer, cfg.GetAgentID(), hmacSecret, cancel)
368+
httpServer := buildHTTPServer(ctx, mode, listenAddr, unifiedServer, cfg.GetAgentIDs(), hmacSecret, cancel)
367369

368370
// Build net.Listener — optionally wrapping with TLS (ASI-07 Phase 1).
369371
// Plain HTTP is still used when no TLS certificate is configured (backward compatible).

internal/cmd/run_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,54 @@ func TestRun_InvalidGuardsMode(t *testing.T) {
215215
require.Error(t, err)
216216
assert.Contains(t, err.Error(), "invalid --guards-mode flag")
217217
}
218+
219+
// TestRun_PluralAgentIDsStartsSuccessfully verifies that run() starts normally
220+
// when only gateway.agentIds (plural) is configured: every configured
221+
// identifier is accepted as a valid Authorization credential (see
222+
// config.Config.GetAgentIDs and authMiddleware's matchesAnyKey), enabling
223+
// concurrent primary/enclave sessions that each authenticate independently.
224+
func TestRun_PluralAgentIDsStartsSuccessfully(t *testing.T) {
225+
resetRunFlagsForTest(t)
226+
227+
dir := t.TempDir()
228+
path := filepath.Join(dir, "config.toml")
229+
content := `[gateway]
230+
port = 3000
231+
agent_ids = ["primary-agent", "enclave-agent"]
232+
233+
[servers.testserver]
234+
type = "http"
235+
url = "http://127.0.0.1:1"
236+
`
237+
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
238+
239+
configFile = path
240+
configStdin = false
241+
envFile = ""
242+
listenAddr = "127.0.0.1:0"
243+
routedMode = true
244+
unifiedMode = false
245+
logDir = t.TempDir()
246+
wasmCacheDir = t.TempDir()
247+
shutdownTimeout = 2 * time.Second
248+
difcMode = "strict"
249+
250+
ctx, cancel := context.WithCancel(context.Background())
251+
rootCmd.SetContext(ctx)
252+
t.Cleanup(func() { rootCmd.SetContext(context.Background()) })
253+
254+
errCh := make(chan error, 1)
255+
go func() {
256+
errCh <- run(rootCmd, nil)
257+
}()
258+
259+
time.Sleep(200 * time.Millisecond)
260+
cancel()
261+
262+
select {
263+
case err := <-errCh:
264+
assert.NoError(t, err, "run() should start successfully with only gateway.agentIds configured")
265+
case <-time.After(5 * time.Second):
266+
t.Fatal("run() did not return within the expected shutdown window")
267+
}
268+
}

internal/cmd/startup.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,20 @@ func buildHTTPServer(
6969
ctx context.Context,
7070
mode, listenAddr string,
7171
unifiedServer *server.UnifiedServer,
72-
agentID, hmacSecret string,
72+
agentIDs []string, hmacSecret string,
7373
cancel context.CancelFunc,
7474
) *http.Server {
7575
debugLog.Printf("Building HTTP server: mode=%s, listenAddr=%s, authEnabled=%v, hmacEnabled=%v",
76-
mode, listenAddr, agentID != "", hmacSecret != "")
76+
mode, listenAddr, len(agentIDs) > 0, hmacSecret != "")
7777
var httpServer *http.Server
7878
if mode == "routed" {
7979
logger.StartupInfo("Starting MCPG in ROUTED mode on %s", listenAddr)
8080
logger.StartupInfo("Routes: /mcp/<server> where <server> is one of: %v", unifiedServer.GetServerIDs())
81-
httpServer = server.CreateHTTPServerForRoutedMode(listenAddr, unifiedServer, agentID, hmacSecret)
81+
httpServer = server.CreateHTTPServerForRoutedMode(listenAddr, unifiedServer, agentIDs, hmacSecret)
8282
} else {
8383
logger.StartupInfo("Starting MCPG in UNIFIED mode on %s", listenAddr)
8484
logger.StartupInfo("Endpoint: /mcp")
85-
httpServer = server.CreateHTTPServerForMCP(listenAddr, unifiedServer, agentID, hmacSecret)
85+
httpServer = server.CreateHTTPServerForMCP(listenAddr, unifiedServer, agentIDs, hmacSecret)
8686
}
8787

8888
// Set BaseContext so every incoming request inherits the startup context,

0 commit comments

Comments
 (0)