You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When OpenCodex routes subagent requests (explorer, shadow calls via gpt-5.6-luna) to sensenova/sensenova-6.8-flash-lite, the model hallucinates tool calls to exec even when exec is not declared in the request tools. This triggers bridge.ts:998 validation failure, causing 502 and stream disconnect.
Error: routed provider emitted undeclared client tool "exec"; only request-declared tools may be called
Reproduction
Configure shadowCallIntercept to route gpt-5.6-luna to sensenova/sensenova-6.8-flash-lite
Codex spawns subagent (explorer) with limited tool set (search_files, read_file, etc., NO exec)
sensenova sees system prompt mentioning exec capability and hallucinates tool call to exec
Undeclared tool calls from third-party providers should be tolerated (dropped with warning) instead of failing the entire turn. This is especially important for subagent/shadow calls where the model may see system prompts describing capabilities not available in the current request tool set.
#304 - fix(kiro): restore hardening and complete text turns [open] #402 - fix(cursor): stop false shell blocked reports and cmd/command mismatch (#399) [merged] #1411 - fix(command-code): pair tool results and synthesize missing-result errors [merged] #1531 - fix(openai-chat): refuse to emit an unnamed streamed tool call [merged] #1788 - fix(responses): fail closed when a routed provider calls an undeclared tool [open]
📝 Issue Planner
Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.
Create Plan
🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your .coderabbit.yaml:
issue_enrichment:
auto_enrich:
enabled: false
💬 Have feedback or questions? Drop into our discord!
The failures here are real, but the proposed fix runs against a decision this repo has already recorded, so it seemed worth flagging before anyone implements it.
structure/04_transports-and-sidecars.md has a decision log for exactly this question. It lists "rely only on prompt guidance" among the rejected alternatives and states the chosen behaviour as:
선택한 방식: Retain the allowed wire-name set with the existing bridge maps and fail the turn with an explicit compatibility error before emitting any undeclared tool item.
with the stated reason:
Model guidance is not an enforcement boundary, while automatic translation would invent executable caller intent and arguments after generation.
and the intended consequence:
providers that emit aliases they never advertised must correct their adapter mapping instead of relying on client abort behavior.
Dropping the call and continuing is the fail-open behaviour that decision moved away from. It would also silently diverge the two transports: bridge.ts:998 would tolerate an undeclared name while the native Responses passthrough refuses it (#1788, which closes that gap in the other direction).
On the mechanism, in case it helps narrow the real fix. The catalog nudge is already being injected on this path — openai-chat.ts calls buildNonOpenAIToolCatalogNudgeForTools whenever shouldInjectNonOpenAIToolCatalogNudge(provider) is true, and sensenova is a non-OpenAI host, so the request should already carry "Valid tool names for this turn are exactly …". What it does not carry is an explicit prohibition on exec: NEIGHBOR_AGENT_TOOL_NAMES lists only Read, Grep, Glob, Bash, LS, and exec is deliberately absent for the same reason apply_patch is — it is Codex's own first-class tool, not a neighbouring harness's.
So the mismatch looks less like "the guard is too strict" and more like "the subagent turn ships a system prompt describing exec while its catalog omits it", and the model believes the prompt over the tool list. That framing suggests fixes that keep the enforcement boundary intact — tightening what the nudge says when a Codex-native tool is described but undeclared, or not describing exec in a subagent prompt that cannot execute it.
I would be careful with the first, though: the comment above NEIGHBOR_AGENT_TOOL_NAMES records that explicitly forbidding apply_patch pushed routed models into python3 heredoc edits instead. Naming exec as forbidden may well buy the same class of workaround.
I have not sent a PR for this because the two readings lead to opposite changes and only a maintainer can pick. If the answer is "keep fail-closed and fix the prompt/catalog mismatch", I am happy to take it — I have been in this code for #1788 and #1805. If the answer is that routed subagent turns should genuinely tolerate undeclared calls, that is a change to the decision log rather than to bridge.ts, and it should probably say so explicitly so the passthrough and bridged paths do not drift apart again.
routed provider emitted undeclared client tool "apply_patch"; only request-declared tools may be called
The attempt ran for ~31.5 s and produced first output after ~28 s before terminating on the undeclared tool call. Usage was unreported because the turn failed at this boundary.
This looks like the same underlying class as the reported undeclared exec call, but it is particularly relevant because apply_patch already has an intentional Code Mode special case in the repo.
Current behavior is internally consistent:
buildToolBridgeMaps() derives declaredToolNames from the request-visible tool catalog.
bridge.ts correctly fails closed when the routed provider emits a name outside that set.
Under Codex Code Mode, patching can be available only as nested exec -> tools.apply_patch(...), while top-level apply_patch is not necessarily a request-declared wire tool.
tool-catalog-nudge.ts deliberately does not put apply_patch in NEIGHBOR_AGENT_TOOL_NAMES, because the earlier explicit prohibition caused routed models to avoid the intended patch path and fall back to Python heredocs/sed.
So this reproduction suggests the remaining problem is the prompt/tool-contract interpretation for routed Code Mode models: Kimi K3 still interpreted the available patch capability as permission to emit a top-level apply_patch call rather than invoking the declared exec surface and using the nested tools.apply_patch(...) helper.
That makes the issue provider-cross-cutting rather than a SenseNova quirk. I would keep the existing fail-closed enforcement boundary. Dropping undeclared calls or auto-translating top-level apply_patch into exec -> tools.apply_patch(...) would either silently lose intended actions or invent executable caller intent after generation.
Likely direction to investigate: make Code Mode guidance distinguish more clearly between top-level callable tool names and helpers available only inside exec, without reinstating the old blanket apply_patch prohibition that caused routed models to switch to heredoc edits.
I opened draft PR #1817 for the fail-closed-compatible fix: #1817
The implementation addresses both reproduced forms without weakening declaredToolNames enforcement:
responses/parser.ts no longer leaks apply_patch patch-envelope argument guidance into every custom tool; only logical apply_patch keeps that text, while Code Mode exec and other custom tools get neutral freeform-input guidance.
the non-OpenAI catalog nudge now defines the listed catalog as the complete top-level invocation surface and explicitly says that names appearing only in instructions/tool metadata/nested helper APIs do not become extra top-level tools. Nested tools.* helpers must be used through their listed parent tool.
undeclared returned names still fail closed. No drop-and-continue and no automatic apply_patch -> exec translation.
The PR includes focused regressions plus the full research trace, including #1544, #1699/#1700, the prior apply_patch-prohibition regression, this issue's discussion, and the v2.22.0 Kimi K3 reproduction:
I left the PR as draft because this tool environment cannot run Bun locally. A live SenseNova/Kimi re-probe plus the focused/typecheck/shared runtime tests should be green before treating #1795 as resolved.
Correction to my earlier Kimi K3 repro comment: that failing run was from a different worktree that I later found was still running OpenCodex v2.14.2, not v2.22.0. I re-ran the same Kimi K3 / openai-chat / xhigh Code Mode scenario on actual v2.22.0 and have not reproduced the undeclared top-level apply_patch failure so far.
The separate static metadata defect discussed in #1817 is still present in v2.22.0/current dev: the Responses parser still attaches apply_patch-specific input guidance to non-apply_patch custom tools, and the non-OpenAI catalog nudge still does not explicitly distinguish the top-level invocation surface from nested helper APIs. So the live Kimi evidence should be treated as v2.14.2 evidence, while the v2.22.0 case remains unconfirmed.
Looked at this closely and it needs a decision from you before any code moves, so here is what the tracing shows.
The guard is deliberate, not an oversight. Both bridge paths refuse an undeclared tool name — streaming at src/bridge.ts:1006, non-streaming at :1715 — and both are pinned by tests (tests/bridge.test.ts:345, tests/responses-stream-tool-events.test.ts:30). So the request here is to relax a safety contract, which is a different thing from fixing a defect.
Why "drop it with a warning" is not obviously the safer option. An exec call the client never declared is by construction one the client has no handler for. Dropping it silently means the model believes it ran a command and gets back either nothing or a fabricated absence, then continues the turn on that false premise. For exec in particular the 502 is the honest answer: the turn genuinely cannot complete as the model intended. Trading a visible failure for an invisible one is not automatically an improvement.
Where the tolerance belongs matters more than whether it exists. Three options, very different blast radii:
Global — any routed provider may emit any tool name and have it dropped. Removes the guard for the cases it was written for.
Per-provider opt-in — a config flag for a known-noisy upstream. Contained, but the operator has to know which providers need it.
Subagent-scope only — narrowest, and closest to your actual case.
Option 3 raises a question worth answering first. You describe the subagent seeing "a system prompt mentioning exec capability" while its declared tool set omits exec. If that is accurate, the request is internally inconsistent before the provider replies — a prompt advertising a capability the tools array does not include. That would make this a request-construction defect with a different and safer fix than loosening the validator.
What I could not establish. No live SenseNova/Kimi reproduction was available, so I cannot tell whether this is provider-specific or general small-model behavior under a capability-describing prompt, and I cannot confirm whether the subagent request really does advertise exec in its prompt while omitting it from tools.
Two things would let this move:
a captured subagent request body (tools array + system prompt, redacted) showing the mismatch;
your call on scope — global, per-provider opt-in, or subagent-only.
Leaving this open rather than guessing at a safety-boundary change. Reasoning recorded in devlog/_plan/260816_wave34_closeout/130_1795_undeclared_tools.md.
This is an unvalidated-tool-call trust boundary failure — the bridge treats "provider emitted a tool call" and "provider emitted a tool call I asked for" as the same event, and only discovers the gap after the stream is already committed.
The issue itself names the mechanism: the model "hallucinates tool calls to exec even when exec is not declared in the request tools," and the report's own framing gives the reason — subagent system prompts describe "capabilities not available in the current request tool set." That's the trigger. sensenova-6.8-flash-lite is pattern-matching on prose in the system prompt (mentions of exec-capable orchestration) rather than the actual tools array in the request payload, and nothing upstream of bridge.ts:998 filters model output against declaredToolNames before it's treated as a committed turn action.
The check that would've caught this isn't at line 998 — it's earlier, at the boundary where the provider stream is parsed into tool-call events. Before any tool-call event is forwarded to turn state, assert declaredToolNames.has(event.name); on failure, the event should be classified as "provider protocol violation, not turn failure" and short-circuited to a no-op/log, not propagated as a stream-fatal error. The fix proposed in the issue (break instead of 502) is directionally right, but the deeper fix is making that assertion a first-class contract at the parse boundary, not a late validation gate — so every routed-provider adapter inherits it instead of each call site remembering to check.
One thing I can't confirm from the issue alone: whether this is sensenova-specific (weaker instruction-following on tool-call constraints) or a general routed-provider issue that just happens to surface here first — that would need the same shadowCallIntercept route tested against a different backing model to isolate provider vs. bridge behavior.
Closing as by-design. When a provider hallucinates a tool that was never declared in the request (exec here), refusing the call and failing the turn is the intended contract — the proxy will not fail-open by fabricating or silently dropping an undeclared invocation, because that would execute or discard actions the caller never authorized. The nudge/parser hardening that landed via #1817 is the supported mitigation surface. If a subagent-specific tolerance mode is ever wanted, that is a product decision for a fresh feature issue, not a bug in the current behavior.
Activity
coderabbitai commentedon Aug 15, 2026
🔗 Related PRs
#304 - fix(kiro): restore hardening and complete text turns [open]
#402 - fix(cursor): stop false shell blocked reports and cmd/command mismatch (
#399) [merged]#1411 - fix(command-code): pair tool results and synthesize missing-result errors [merged]
#1531 - fix(openai-chat): refuse to emit an unnamed streamed tool call [merged]
#1788 - fix(responses): fail closed when a routed provider calls an undeclared tool [open]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
ntdatt812 commentedon Aug 16, 2026
The failures here are real, but the proposed fix runs against a decision this repo has already recorded, so it seemed worth flagging before anyone implements it.
structure/04_transports-and-sidecars.mdhas a decision log for exactly this question. It lists "rely only on prompt guidance" among the rejected alternatives and states the chosen behaviour as:with the stated reason:
and the intended consequence:
Dropping the call and continuing is the fail-open behaviour that decision moved away from. It would also silently diverge the two transports:
bridge.ts:998would tolerate an undeclared name while the native Responses passthrough refuses it (#1788, which closes that gap in the other direction).On the mechanism, in case it helps narrow the real fix. The catalog nudge is already being injected on this path —
openai-chat.tscallsbuildNonOpenAIToolCatalogNudgeForToolswhenevershouldInjectNonOpenAIToolCatalogNudge(provider)is true, and sensenova is a non-OpenAI host, so the request should already carry "Valid tool names for this turn are exactly …". What it does not carry is an explicit prohibition onexec:NEIGHBOR_AGENT_TOOL_NAMESlists onlyRead,Grep,Glob,Bash,LS, andexecis deliberately absent for the same reasonapply_patchis — it is Codex's own first-class tool, not a neighbouring harness's.So the mismatch looks less like "the guard is too strict" and more like "the subagent turn ships a system prompt describing
execwhile its catalog omits it", and the model believes the prompt over the tool list. That framing suggests fixes that keep the enforcement boundary intact — tightening what the nudge says when a Codex-native tool is described but undeclared, or not describingexecin a subagent prompt that cannot execute it.I would be careful with the first, though: the comment above
NEIGHBOR_AGENT_TOOL_NAMESrecords that explicitly forbiddingapply_patchpushed routed models intopython3heredoc edits instead. Namingexecas forbidden may well buy the same class of workaround.I have not sent a PR for this because the two readings lead to opposite changes and only a maintainer can pick. If the answer is "keep fail-closed and fix the prompt/catalog mismatch", I am happy to take it — I have been in this code for #1788 and #1805. If the answer is that routed subagent turns should genuinely tolerate undeclared calls, that is a change to the decision log rather than to
bridge.ts, and it should probably say so explicitly so the passthrough and bridged paths do not drift apart again.Wibias commentedon Aug 16, 2026
Additional reproduction confirms this is not SenseNova-specific and also affects the Codex-native
apply_patchCode Mode boundary.Observed on current latest stable OpenCodex v2.22.0 (2026-08-16) with an explicit provider route:
teamwicked-kimi-k3/teamwicked-kimi-k3openai-chatxhighexplicit-providerupstream_server_errorThe attempt ran for ~31.5 s and produced first output after ~28 s before terminating on the undeclared tool call. Usage was unreported because the turn failed at this boundary.
This looks like the same underlying class as the reported undeclared
execcall, but it is particularly relevant becauseapply_patchalready has an intentional Code Mode special case in the repo.Current behavior is internally consistent:
buildToolBridgeMaps()derivesdeclaredToolNamesfrom the request-visible tool catalog.bridge.tscorrectly fails closed when the routed provider emits a name outside that set.exec -> tools.apply_patch(...), while top-levelapply_patchis not necessarily a request-declared wire tool.tool-catalog-nudge.tsdeliberately does not putapply_patchinNEIGHBOR_AGENT_TOOL_NAMES, because the earlier explicit prohibition caused routed models to avoid the intended patch path and fall back to Python heredocs/sed.So this reproduction suggests the remaining problem is the prompt/tool-contract interpretation for routed Code Mode models: Kimi K3 still interpreted the available patch capability as permission to emit a top-level
apply_patchcall rather than invoking the declaredexecsurface and using the nestedtools.apply_patch(...)helper.That makes the issue provider-cross-cutting rather than a SenseNova quirk. I would keep the existing fail-closed enforcement boundary. Dropping undeclared calls or auto-translating top-level
apply_patchintoexec -> tools.apply_patch(...)would either silently lose intended actions or invent executable caller intent after generation.Likely direction to investigate: make Code Mode guidance distinguish more clearly between top-level callable tool names and helpers available only inside
exec, without reinstating the old blanketapply_patchprohibition that caused routed models to switch to heredoc edits.Wibias commentedon Aug 16, 2026
I opened draft PR #1817 for the fail-closed-compatible fix: #1817
The implementation addresses both reproduced forms without weakening
declaredToolNamesenforcement:responses/parser.tsno longer leaksapply_patchpatch-envelope argument guidance into every custom tool; only logicalapply_patchkeeps that text, while Code Modeexecand other custom tools get neutral freeform-input guidance.tools.*helpers must be used through their listed parent tool.apply_patch -> exectranslation.The PR includes focused regressions plus the full research trace, including #1544, #1699/#1700, the prior
apply_patch-prohibition regression, this issue's discussion, and the v2.22.0 Kimi K3 reproduction:devlog/_plan/260816_undeclared_tool_invocation_contract/000_research.mdI left the PR as draft because this tool environment cannot run Bun locally. A live SenseNova/Kimi re-probe plus the focused/typecheck/shared runtime tests should be green before treating #1795 as resolved.
Wibias commentedon Aug 16, 2026
Correction to my earlier Kimi K3 repro comment: that failing run was from a different worktree that I later found was still running OpenCodex v2.14.2, not v2.22.0. I re-ran the same Kimi K3 /
openai-chat/xhighCode Mode scenario on actual v2.22.0 and have not reproduced the undeclared top-levelapply_patchfailure so far.The separate static metadata defect discussed in #1817 is still present in v2.22.0/current dev: the Responses parser still attaches
apply_patch-specific input guidance to non-apply_patchcustom tools, and the non-OpenAI catalog nudge still does not explicitly distinguish the top-level invocation surface from nested helper APIs. So the live Kimi evidence should be treated as v2.14.2 evidence, while the v2.22.0 case remains unconfirmed.lidge-jun commentedon Aug 16, 2026
Looked at this closely and it needs a decision from you before any code moves, so here is what the tracing shows.
The guard is deliberate, not an oversight. Both bridge paths refuse an undeclared tool name — streaming at
src/bridge.ts:1006, non-streaming at:1715— and both are pinned by tests (tests/bridge.test.ts:345,tests/responses-stream-tool-events.test.ts:30). So the request here is to relax a safety contract, which is a different thing from fixing a defect.Why "drop it with a warning" is not obviously the safer option. An
execcall the client never declared is by construction one the client has no handler for. Dropping it silently means the model believes it ran a command and gets back either nothing or a fabricated absence, then continues the turn on that false premise. Forexecin particular the 502 is the honest answer: the turn genuinely cannot complete as the model intended. Trading a visible failure for an invisible one is not automatically an improvement.Where the tolerance belongs matters more than whether it exists. Three options, very different blast radii:
Option 3 raises a question worth answering first. You describe the subagent seeing "a system prompt mentioning exec capability" while its declared tool set omits
exec. If that is accurate, the request is internally inconsistent before the provider replies — a prompt advertising a capability thetoolsarray does not include. That would make this a request-construction defect with a different and safer fix than loosening the validator.What I could not establish. No live SenseNova/Kimi reproduction was available, so I cannot tell whether this is provider-specific or general small-model behavior under a capability-describing prompt, and I cannot confirm whether the subagent request really does advertise
execin its prompt while omitting it fromtools.Two things would let this move:
Leaving this open rather than guessing at a safety-boundary change. Reasoning recorded in
devlog/_plan/260816_wave34_closeout/130_1795_undeclared_tools.md.docs(devlog): record the Wave 3/4 second pass, audit corrections and …
impartshadow commentedon Aug 17, 2026
This is an unvalidated-tool-call trust boundary failure — the bridge treats "provider emitted a tool call" and "provider emitted a tool call I asked for" as the same event, and only discovers the gap after the stream is already committed.
The issue itself names the mechanism: the model "hallucinates tool calls to
execeven whenexecis not declared in the request tools," and the report's own framing gives the reason — subagent system prompts describe "capabilities not available in the current request tool set." That's the trigger. sensenova-6.8-flash-lite is pattern-matching on prose in the system prompt (mentions of exec-capable orchestration) rather than the actualtoolsarray in the request payload, and nothing upstream of bridge.ts:998 filters model output againstdeclaredToolNamesbefore it's treated as a committed turn action.The check that would've caught this isn't at line 998 — it's earlier, at the boundary where the provider stream is parsed into tool-call events. Before any tool-call event is forwarded to turn state, assert
declaredToolNames.has(event.name); on failure, the event should be classified as "provider protocol violation, not turn failure" and short-circuited to a no-op/log, not propagated as a stream-fatal error. The fix proposed in the issue (break instead of 502) is directionally right, but the deeper fix is making that assertion a first-class contract at the parse boundary, not a late validation gate — so every routed-provider adapter inherits it instead of each call site remembering to check.One thing I can't confirm from the issue alone: whether this is sensenova-specific (weaker instruction-following on tool-call constraints) or a general routed-provider issue that just happens to surface here first — that would need the same shadowCallIntercept route tested against a different backing model to isolate provider vs. bridge behavior.
Advisory SFA-2026-EE235C1DD2 · Full redacted write-up: https://impartshadow.github.io/echo-site/failure-cases/bug-sensenova-hallucinates-undeclared-exec-tool-in-s-ee235c1dd2.html — I triage one of these in public
every day.
If this diagnosis is useful, react 👍 to this comment; if it is wrong, react 👎 — I use the verdict to choose what I investigate next.
lidge-jun commentedon Aug 18, 2026
Closing as by-design. When a provider hallucinates a tool that was never declared in the request (
exechere), refusing the call and failing the turn is the intended contract — the proxy will not fail-open by fabricating or silently dropping an undeclared invocation, because that would execute or discard actions the caller never authorized. The nudge/parser hardening that landed via #1817 is the supported mitigation surface. If a subagent-specific tolerance mode is ever wanted, that is a product decision for a fresh feature issue, not a bug in the current behavior.docs(devlog): record the Wave 3/4 second pass, audit corrections and …