Summary
The gateway's OpenAI-compatible chat-completions endpoint (POST /v1/chat/completions) silently runs a well-formed but unknown agent target under agents.defaults (the root persona) instead of returning a 4xx. There is no roster validation of the resolved agent id. This produces "phantom agent" replies that are indistinguishable from a real agent to the calling integration.
Verified against the gateway 2026.5.26 dist.
Two distinct gaps
(a) Unknown model: openclaw/<slug> falls through to agents.defaults
When a client sends model: openclaw/<slug> (or openclaw:<slug>, agent:<slug>) where <slug> is not a configured agent, resolveAgentIdForRequest (in the gateway HTTP http-utils) resolves it to the default agent and the turn executes under agents.defaults with a synthesized workspace. No error is surfaced.
This is especially dangerous for dashed-GUID slugs: a malformed integration that passes a conversation/user GUID as the agent slug gets a fully successful 200 response generated by the root persona. (This is the shape of the 2026-05-17 phantom-agent incident we hit downstream: an Office DM created against a non-roster slug produced a confident reply attributed to a "team agent" that does not exist.)
Expected: a well-formed request whose model names an unknown agent slug should return a structured 4xx (e.g. 404 unknown_agent / 422), not a 200 from the default agent. Silent fallback to agents.defaults should at minimum be opt-in (config flag), not the default behavior.
(b) x-openclaw-agent-id header is sanitized but never roster-validated
The x-openclaw-agent-id override header takes precedence over the model field. It is sanitized (format/charset) but the resolved agent id is never checked against the configured agent roster. So the same phantom-agent fallthrough happens via the header path, and it bypasses any validation a caller might expect from the model field.
Expected: the header-resolved agent id should be roster-validated identically to the model-resolved id; an unknown id should 4xx.
Impact
Integrations that build the model/x-openclaw-agent-id value from user- or data-derived input (conversation routing, DM creation, etc.) cannot distinguish "agent ran" from "unknown slug silently ran as root". The only safe workaround today is to validate the slug against a roster snapshot before calling the gateway — which is what we've now done on our side (ByteDesk BDP-1986), but the gateway should fail closed on unknown agent targets.
Suggested fix
- In
resolveAgentIdForRequest, after resolving the target agent id (from model or x-openclaw-agent-id), verify it exists in the configured agent set. If not, return a structured 4xx instead of falling back to agents.defaults.
- Gate any intentional default-fallback behind an explicit config flag (default off).
Summary
The gateway's OpenAI-compatible chat-completions endpoint (
POST /v1/chat/completions) silently runs a well-formed but unknown agent target underagents.defaults(the root persona) instead of returning a 4xx. There is no roster validation of the resolved agent id. This produces "phantom agent" replies that are indistinguishable from a real agent to the calling integration.Verified against the gateway
2026.5.26dist.Two distinct gaps
(a) Unknown
model: openclaw/<slug>falls through toagents.defaultsWhen a client sends
model: openclaw/<slug>(oropenclaw:<slug>,agent:<slug>) where<slug>is not a configured agent,resolveAgentIdForRequest(in the gateway HTTPhttp-utils) resolves it to the default agent and the turn executes underagents.defaultswith a synthesized workspace. No error is surfaced.This is especially dangerous for dashed-GUID slugs: a malformed integration that passes a conversation/user GUID as the agent slug gets a fully successful 200 response generated by the root persona. (This is the shape of the 2026-05-17 phantom-agent incident we hit downstream: an Office DM created against a non-roster slug produced a confident reply attributed to a "team agent" that does not exist.)
Expected: a well-formed request whose
modelnames an unknown agent slug should return a structured4xx(e.g.404 unknown_agent/422), not a 200 from the default agent. Silent fallback toagents.defaultsshould at minimum be opt-in (config flag), not the default behavior.(b)
x-openclaw-agent-idheader is sanitized but never roster-validatedThe
x-openclaw-agent-idoverride header takes precedence over themodelfield. It is sanitized (format/charset) but the resolved agent id is never checked against the configured agent roster. So the same phantom-agent fallthrough happens via the header path, and it bypasses any validation a caller might expect from themodelfield.Expected: the header-resolved agent id should be roster-validated identically to the
model-resolved id; an unknown id should 4xx.Impact
Integrations that build the
model/x-openclaw-agent-idvalue from user- or data-derived input (conversation routing, DM creation, etc.) cannot distinguish "agent ran" from "unknown slug silently ran as root". The only safe workaround today is to validate the slug against a roster snapshot before calling the gateway — which is what we've now done on our side (ByteDesk BDP-1986), but the gateway should fail closed on unknown agent targets.Suggested fix
resolveAgentIdForRequest, after resolving the target agent id (frommodelorx-openclaw-agent-id), verify it exists in the configured agent set. If not, return a structured 4xx instead of falling back toagents.defaults.