Skip to content

fix(gateway): reject unknown agent IDs on OpenAI + OpenResponses HTTP paths#45344

Closed
spence27 wants to merge 1 commit into
openclaw:mainfrom
spence27:fix/gateway-reject-unknown-agent-ids
Closed

fix(gateway): reject unknown agent IDs on OpenAI + OpenResponses HTTP paths#45344
spence27 wants to merge 1 commit into
openclaw:mainfrom
spence27:fix/gateway-reject-unknown-agent-ids

Conversation

@spence27

Copy link
Copy Markdown

Problem

Unknown agent IDs passed via x-openclaw-agent-id, x-openclaw-agent, or model selectors like openclaw:ghost / agent:ghost were silently accepted and routed to main instead of being rejected.

Repro on the installed gateway:

x-openclaw-agent-id: definitely-not-a-real-agent → 200 PONG   (wrong)
x-openclaw-agent-id: <unconfigured> → 200 PONG                (wrong)

Fix

  • src/gateway/http-utils.ts — adds InvalidGatewayAgentIdError and wires it into resolveAgentIdForRequest / resolveGatewayRequestContext when a knownAgentIds list is supplied. Implicit fallback to main (no header, no model selector) is unchanged.
  • src/gateway/openai-http.ts — passes listAgentIds(loadConfig()) as knownAgentIds and maps InvalidGatewayAgentIdErrorsendInvalidRequest(400).
  • src/gateway/openresponses-http.ts — same change for /v1/responses.

Tests

All 16 tests pass locally (vitest.gateway.config.ts):

File Tests
http-utils.request-context.test.ts 7 (4 new: defaults-to-main, known header, unknown header rejected, unknown model rejected)
openai-http.test.ts 4 (2 new: ghost via header + ghost via model → 400)
openresponses-http.test.ts 5 (3 new: default-routes-main, ghost via header → 400, ghost via model → 400)

Error shape (consistent with existing gateway errors)

{
  "error": {
    "type": "invalid_request_error",
    "message": "Unknown agent id \"ghost\". Use \"openclaw agents list\" to see configured agents."
  }
}

… paths

Unknown agent IDs supplied via x-openclaw-agent-id / x-openclaw-agent or
model selectors like openclaw:ghost / agent:ghost now return 400
invalid_request_error instead of silently routing to main.

- Add InvalidGatewayAgentIdError to http-utils
- resolveAgentIdForRequest validates explicit selections against
  knownAgentIds when provided; implicit fallback to main unchanged
- openai-http and openresponses-http pass listAgentIds(loadConfig()) as
  knownAgentIds and catch the error as sendInvalidRequest(400)
- Tests: 7 unit tests in request-context test, 9 e2e tests across both
  HTTP handler test files; all 16 pass
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: M labels Mar 13, 2026
@greptile-apps

greptile-apps Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a security gap in the OpenClaw gateway where unknown agent IDs supplied via x-openclaw-agent-id, x-openclaw-agent, or model-selector strings (openclaw:<id> / agent:<id>) were silently routed to the main agent rather than rejected. The fix adds InvalidGatewayAgentIdError, validates explicit selections against listAgentIds(loadConfig()) in both HTTP handlers, and maps the error to a consistent 400 JSON response.

Key changes:

  • http-utils.ts — new InvalidGatewayAgentIdError class; internal ExplicitAgentSelection type preserves raw input for error messages; assertKnownExplicitAgentSelection enforces the allowlist; no-header / no-model fallback to main is preserved.
  • openai-http.ts / openresponses-http.ts — both handlers now wrap resolveGatewayRequestContext in a try/catch that converts InvalidGatewayAgentIdError to a 400 invalid_request_error.
  • Tests cover four new scenarios across three test files.

One logic concern:
assertKnownExplicitAgentSelection is called with the optional knownAgentIds parameter, but when that parameter is undefined, the optional-chain expression knownAgentIds?.includes(...) evaluates to undefined and !undefined === true causes the function to always throw for any explicit agent selection. Both current callers always supply a non-undefined list, so nothing breaks today, but any future gateway handler or test that omits knownAgentIds while handling agent-scoped requests will get unexpected InvalidGatewayAgentIdError exceptions. Treating undefined as "skip validation" (i.e. if (knownAgentIds !== undefined && !knownAgentIds.includes(...))) would make the optional contract safe.

Confidence Score: 4/5

  • Safe to merge with the optional-parameter footgun acknowledged — all current callers pass a valid knownAgentIds list.
  • The security fix is correct and well-tested. The only concern is an API design issue in assertKnownExplicitAgentSelection: omitting the optional knownAgentIds parameter (rather than "no validation") causes it to throw on any explicit agent selection, which contradicts the ? optional contract. This does not affect any current production path but is a subtle footgun for future contributors.
  • src/gateway/http-utils.ts — specifically the assertKnownExplicitAgentSelection helper and its behavior when knownAgentIds is undefined.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/http-utils.ts
Line: 80-88

Comment:
**`knownAgentIds: undefined` silently rejects all explicit agent selections**

When `knownAgentIds` is `undefined`, the optional-chaining expression `knownAgentIds?.includes(selection.agentId)` evaluates to `undefined`, so `!undefined` is `true` and the function **always throws `InvalidGatewayAgentIdError`** — even for a perfectly valid agent ID.

This contradicts the implied contract of the `?` optional parameter in `resolveAgentIdForRequest` and `resolveGatewayRequestContext`: a caller who omits `knownAgentIds` expecting "no validation" will instead get an unexpected error thrown for any request that carries an explicit `x-openclaw-agent-id` header or an `openclaw:<id>` / `agent:<id>` model selector.

The two current production callers always pass `listAgentIds(loadConfig())`, so nothing breaks today. However, any future gateway handler (or unit test) that omits `knownAgentIds` while handling agent-scoped requests will silently break.

Suggested fix — treat `undefined` as "skip validation":

```ts
function assertKnownExplicitAgentSelection(
  selection: ExplicitAgentSelection,
  knownAgentIds: readonly string[] | undefined,
): string {
  if (knownAgentIds !== undefined && !knownAgentIds.includes(selection.agentId)) {
    throw new InvalidGatewayAgentIdError(selection.rawAgentId);
  }
  return selection.agentId;
}
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 3eec497

Comment thread src/gateway/http-utils.ts
Comment on lines +80 to 88
function assertKnownExplicitAgentSelection(
selection: ExplicitAgentSelection,
knownAgentIds: readonly string[] | undefined,
): string {
if (!knownAgentIds?.includes(selection.agentId)) {
throw new InvalidGatewayAgentIdError(selection.rawAgentId);
}
return selection.agentId;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

knownAgentIds: undefined silently rejects all explicit agent selections

When knownAgentIds is undefined, the optional-chaining expression knownAgentIds?.includes(selection.agentId) evaluates to undefined, so !undefined is true and the function always throws InvalidGatewayAgentIdError — even for a perfectly valid agent ID.

This contradicts the implied contract of the ? optional parameter in resolveAgentIdForRequest and resolveGatewayRequestContext: a caller who omits knownAgentIds expecting "no validation" will instead get an unexpected error thrown for any request that carries an explicit x-openclaw-agent-id header or an openclaw:<id> / agent:<id> model selector.

The two current production callers always pass listAgentIds(loadConfig()), so nothing breaks today. However, any future gateway handler (or unit test) that omits knownAgentIds while handling agent-scoped requests will silently break.

Suggested fix — treat undefined as "skip validation":

function assertKnownExplicitAgentSelection(
  selection: ExplicitAgentSelection,
  knownAgentIds: readonly string[] | undefined,
): string {
  if (knownAgentIds !== undefined && !knownAgentIds.includes(selection.agentId)) {
    throw new InvalidGatewayAgentIdError(selection.rawAgentId);
  }
  return selection.agentId;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/http-utils.ts
Line: 80-88

Comment:
**`knownAgentIds: undefined` silently rejects all explicit agent selections**

When `knownAgentIds` is `undefined`, the optional-chaining expression `knownAgentIds?.includes(selection.agentId)` evaluates to `undefined`, so `!undefined` is `true` and the function **always throws `InvalidGatewayAgentIdError`** — even for a perfectly valid agent ID.

This contradicts the implied contract of the `?` optional parameter in `resolveAgentIdForRequest` and `resolveGatewayRequestContext`: a caller who omits `knownAgentIds` expecting "no validation" will instead get an unexpected error thrown for any request that carries an explicit `x-openclaw-agent-id` header or an `openclaw:<id>` / `agent:<id>` model selector.

The two current production callers always pass `listAgentIds(loadConfig())`, so nothing breaks today. However, any future gateway handler (or unit test) that omits `knownAgentIds` while handling agent-scoped requests will silently break.

Suggested fix — treat `undefined` as "skip validation":

```ts
function assertKnownExplicitAgentSelection(
  selection: ExplicitAgentSelection,
  knownAgentIds: readonly string[] | undefined,
): string {
  if (knownAgentIds !== undefined && !knownAgentIds.includes(selection.agentId)) {
    throw new InvalidGatewayAgentIdError(selection.rawAgentId);
  }
  return selection.agentId;
}
```

How can I resolve this? If you propose a fix, please make it concise.

@xkonjin xkonjin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review — Reject Unknown Agent IDs

Overall: Clean, well-tested security fix. The pattern of validating agent IDs at the HTTP boundary before routing is the right approach. Good test coverage with both header and model-based selection paths.

✅ Strengths

  • Custom error class (InvalidGatewayAgentIdError) with actionable CLI hint is great UX
  • Tests cover all four paths: header-known, header-unknown, model-known, model-unknown
  • Consistent application across both OpenAI and OpenResponses HTTP handlers

🟡 Potential issue: loadConfig() called per-request

Both openai-http.ts and openresponses-http.ts call listAgentIds(loadConfig()) inside the request handler. If loadConfig() re-reads from disk on every call, this adds I/O per request. If it's cached/memoized, no issue — worth confirming.

🟡 Missing: test for empty knownAgentIds

What happens when knownAgentIds is undefined or an empty array? Looking at assertKnownExplicitAgentSelection:

if (!knownAgentIds?.includes(selection.agentId)) {

If knownAgentIds is undefined, the optional chain returns undefined (falsy), which means ALL explicit agent selections would throw. That seems intentional (strict mode), but a test confirming this behavior would be valuable.

💡 Minor: duplicate expect blocks in unit tests

The unit tests for unknown agent IDs call the same function twice — once to check toThrowError(InvalidGatewayAgentIdError) and once to check .toThrow(/Unknown agent id/). These could be combined into a single assertion block since toThrowError accepts both a class and a message pattern.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3eec49784d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/gateway/http-utils.ts
selection: ExplicitAgentSelection,
knownAgentIds: readonly string[] | undefined,
): string {
if (!knownAgentIds?.includes(selection.agentId)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Allow explicit main agent selection

Explicitly selecting main now returns a 400 in multi-agent configs that do not list a main entry, because this check only accepts IDs present in knownAgentIds; however, the same request with no selector still falls back to "main" later in this function. That creates a behavior regression where x-openclaw-agent-id: main / model: "openclaw:main" fail even though implicit default routing still targets main, which can break existing clients and documented examples that pin main explicitly.

Useful? React with 👍 / 👎.

@JoeProAI

Copy link
Copy Markdown

Reviewed the diff — solid security fix. The silent fallback to main for unknown agent IDs is a real gap for anyone exposing the HTTP API. Clean implementation, good test coverage across all four paths (header-known, header-unknown, model-known, model-unknown), and the CLI hint in the error message is a nice UX touch.

Agreeing with xkonjin's note on loadConfig() per-request — fine for single-user setups, but worth considering a short-TTL memoization if this path gets hot in multi-tenant deployments. Not a blocker.

Also +1 on the knownAgentIds: undefined behavior question — rejecting all explicit selections when the list is undefined is the safe default, but a brief code comment documenting that intent would help future contributors.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Keep open. The PR addresses a real source-reproducible gateway bug, but the branch is now conflicting and stale, lacks after-fix real behavior proof, and would regress current OpenAI-compatible gateway routing contracts if merged as submitted.

Canonical path: Close this stale PR. The latest review rated it F, the branch still lacks merge-ready proof, and there has been no human follow-up after the durable review.

So I’m closing this here because the remaining work is already tracked in the canonical issue.

Review details

Best possible solution:

Close this stale PR. The latest review rated it F, the branch still lacks merge-ready proof, and there has been no human follow-up after the durable review.

Do we have a high-confidence way to reproduce the issue?

Yes, source-level. Current main resolves explicit header/model agent IDs without configured-agent validation before dispatch; I did not run a live gateway in this read-only review.

Is this the best way to solve the issue?

No as submitted. HTTP-boundary validation is the right direction, but this branch must be rebased and preserve current gateway contracts before it is a maintainable merge shape.

Security review:

Security review cleared: No dependency, workflow, secret-handling, package-resolution, download, or lifecycle-hook changes were found; gateway routing risks are covered as functional merge risks.

What I checked:

  • stale F-rated PR: PR was opened 2026-03-13T17:44:54Z, is older than 60 days, and the latest review rated it F.
  • proof blocker: real behavior proof is missing and proof tier is F, so this branch is not merge-ready without contributor follow-up.
  • no human follow-up: live comments and timeline hydrated by apply contain no non-automation activity after the ClawSweeper review.

Likely related people:

  • vincentkoc: Introduced the agent-first OpenAI-compatible model contract and touched the affected helper, handlers, tests, and docs. (role: feature introducer / recent area contributor; confidence: high; commits: d10669629d73, eaad4ad1be38; files: src/gateway/http-utils.ts, src/gateway/openai-http.ts, src/gateway/openresponses-http.ts)
  • steipete: Introduced the original OpenAI-compatible HTTP endpoint and appears repeatedly in recent gateway HTTP history around this surface. (role: original endpoint contributor / heavy adjacent contributor; confidence: high; commits: dafa8a2881e9, 3ff56020b16f; files: src/gateway/openai-http.ts, src/gateway/http-utils.ts, docs/gateway/openai-http-api.md)
  • Ryan Lisse: Introduced the /v1/responses endpoint that shares the affected request-context and message-channel behavior. (role: Responses endpoint introducer; confidence: medium; commits: f4b03599f0fb; files: src/gateway/openresponses-http.ts, src/gateway/openresponses-http.test.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against cd1cae5be9ba.

@clawsweeper clawsweeper Bot added the P2 Normal backlog priority with limited blast radius. label May 16, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 16, 2026
@clawsweeper clawsweeper Bot added impact:message-loss Channel message delivery can be lost, duplicated, or misrouted. impact:auth-provider Auth, provider routing, model choice, or SecretRef resolution may break. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. and removed impact:message-loss Channel message delivery can be lost, duplicated, or misrouted. impact:auth-provider Auth, provider routing, model choice, or SecretRef resolution may break. labels May 17, 2026
@clawsweeper

clawsweeper Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@clawsweeper

clawsweeper Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants