Repository navigation
[codebuddy] Routed tool-call markup reaches the client as assistant text (no scaffolding guard, unlike #4190) #4596
Description
Client or integration
Codex App
Area
Provider adapter
Summary
Routing an agent-style turn through the codebuddy adapter streams the routed model's raw tool-call markup into the assistant text instead of model output. The markup arrives as plain assistant text, not as a typed tool_use frame.
The route launches the vendor CLI with tools and MCP disabled, so the routed model has no native tool channel to put a call in. It writes the call spelling as prose, and content_block_delta text frames are forwarded to the client unrepaired - the same one line of trust #4190 found for qoder:
src/adapters/coding-agent/protocol.ts:175- thetext_deltaemit on the full-message pathsrc/adapters/coding-agent/protocol.ts:251- the same emit inside thecontent_block_deltabranch
CodeBuddy runs the same runCodingAgentTurn as Qoder, and #4234 (0589579b7) added src/adapters/qoder/scaffold-guard.ts for qoder only. src/adapters/codebuddy/ on dev still contains just adapter.ts and profiles.ts, and src/adapters/coding-agent/ has no filter.
Expected instead: the codebuddy route is the same tools-disabled, text-only surface as qoder. Either the call spelling should be turned into a real tool call (the adapter's own comment calls the control-protocol tool bridge a fast-follow), or the markup should fail closed the way it does for qoder.
Reproduction
- Configure a provider that uses adapter
codebuddy(here:codebuddy-cn). - Route an agent-style Codex turn through it - e.g. ask Codex to inspect git state and read named files in a local repo.
- Observe the assistant text in Codex containing the markup in the log section below.
Intermittent, but it hit 2 of the 2 consecutive agentic turns in the captured session, roughly 35 seconds apart.
Version
2.54.0
Operating system
macOS 26.5.1 (Darwin 25.5.0, arm64), Node v26.5.0
Provider and model
codebuddy (provider id codebuddy-cn) / deepseek-v4.1-flash
Logs or error output
Both blocks are assistant output_text verbatim, captured on the published 2.54.0 build. Two consecutive turns: 2026-09-14T00:42:51Z and 2026-09-14T00:43:26Z (08:42:51 and 08:43:26 CST). Local absolute paths are replaced with [local-path].
Turn 1:
I'll start with a read-only reconnaissance pass: git state, the docs you named, and the actual Android recording/upload code paths.
<||DSML|| calls>
<||DSML|| invoke name="functions.exec">
<||DSML|| parameter name="command" string="true">cd [local-path] && git status --short --branch && echo "---HEAD---" && git rev-parse HEAD && echo "---REMOTE---" && git rev-parse origin/main 2>/dev/null && echo "---WT---" && git worktree list --porcelain | head -40</||DSML|| parameter>
</||DSML|| invoke>
</||DSML|| calls>
Turn 2:
I'll run the real commands now.
<||DSML|| calls>
<||DSML|| invoke name="functions.exec">
<||DSML|| parameter name="cmd" string="true">cd [local-path] && git status --short --branch && echo "---HEAD---" && git rev-parse HEAD && echo "---ORIGIN_MAIN---" && git rev-parse origin/main</||DSML|| parameter>
</||DSML|| invoke>
</||DSML|| calls>
Notes on the shape:
- Every tag is wrapped in full-width bars (U+FF5C), not ASCII pipes. The opener is
<||DSML|| invoke name="functions.exec">, where those bars sit between the angle bracket and the tag name. None of theUNREPAIRABLE_MARKERSshipped for qoder match this leak - see the verification comment below. A signature extension is needed, not only a port of the existing filter. - Turn 2's parameter body is empty, and the surrounding prose ("I'll run the real commands now.") reads as agent narration rather than the model's answer.
opencodex usagerows for the same window report providercodebuddy-cn, modeldeepseek-v4.1-flash, status 200, inbound protocolresponses.- Both turns ran against the freshly installed published build: the 2.50.0 -> 2.54.0 update finished at
2026-09-14T00:33Z(~/.opencodex/update-job.json), about nine minutes earlier, and no local modification had been applied to the package.
Screenshots and supporting files
src/adapters/codebuddy/adapter.tsbuilds args with--tools ""and--strict-mcp-config, so no native tool channel is open, and its own doc comment records: "Codex's tool catalog is not advertised in v1 (the control-protocol tool bridge is a fast-follow)."src/adapters/coding-agent/protocol.tsfilters frame types but never inspects frame contents before emittingtext_delta.- The lane packet for [qoder] Vendor CLI agent scaffolding leaks into routed output (MCP lazy-loading reminder + tool-call markup) #4190 anticipated this report (
devlog/_plan/260911_l6_streaming_tools/020_4190_qoder_scaffold_guard.md): "Qoder only, not the shared coding-agent path. ... If CodeBuddy turns out to do the same thing, the filter is a pure module and lifting it is a small change - but it should be driven by a report, not by symmetry." - Source session log:
~/.codex/sessions/2026/09/14/rollout-2026-09-14T08-42-42-01a09d5d-c356-7b82-9d40-bb70dba9d487.jsonl,response_item->message, roleassistant,content[0].type == "output_text", lines 13 and 26.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Sep 14, 2026
Issue reopened
The report now contains the information required by the automated check. Thanks for updating it.
lidge-jun commentedon Sep 14, 2026
리뷰 · 우선순위 54 / 80
설명
이 이슈는 codebuddy 어댑터 경로에서 벤더 CLI 가 만든 tool-call 마크업이 어시스턴트 텍스트로 그대로 클라이언트로 새는 버그 보고입니다. PR 이 아니라 이슈입니다. 재현은
codebuddy-cn+ agent 스타일 Codex 턴에서 DSML/functions.exec형태 마크업이output_text로 두 번 연속 잡힌 로그입니다.현재
dev를 보면 원인 구조가 이슈 설명과 맞습니다.src/adapters/codebuddy/adapter.ts는runCodingAgentTurn을 그대로 쓰고, 폴더에는adapter.ts와profiles.ts만 있습니다. 같은 공유 스트림 파서src/adapters/coding-agent/protocol.ts는 대략 175줄(full-message)과 251줄(content_block_delta/text_delta)에서 텍스트를 검사 없이 그대로 내보냅니다.비교 대상은 이미 닫힌 qoder 사례입니다.
#4190은 CLOSED 이고,#4234(0589579b7) 가src/adapters/qoder/scaffold-guard.ts를 넣어 벤더 스캐폴딩이 텍스트 채널에 오면 fail-closed 합니다.src/adapters/qoder/adapter.ts는 그 가드를 outbound 채널에 감쌉니다. codebuddy 는 같은 “tools/MCP 끄고 텍스트만” 계약인데 가드가 없습니다. 그래서 모델이 기억한 tool-call 철자를 산문으로 쓰면, 공유 파서가 그대로 클라이언트에 전달합니다.이 이슈가 중요한 이유는 라우팅/풀 선택 버그가 아니라 “텍스트 전용 표면” 계약 위반이기 때문입니다. 운영자·사용자는 tool_use 프레임을 기대하거나, 최소한 마크업이 본문에 섞이지 않기를 기대합니다. qoder 에서 이미 한 번 막은 구멍이 형제 어댑터에서 다시 열린 형태입니다.
현재
dev방향(cost-guard, affinity reason, send-budget 문서)과 직접 겹치지는 않습니다. 다만 coding-agent 계열 어댑터 품질 이슈로, qoder 가드 이식 또는 공유 레이어 승격이 필요합니다. types.ts/config.ts 분할과도 무관합니다.#4190과 증상은 같고 대상 어댑터만 다르므로, qoder 이슈를 다시 열 필요는 없고 codebuddy 전용으로 두는 것이 맞습니다.경로
src/adapters/codebuddy/- scaffold-guard 파일이 없습니다. qoder 와 달리 fail-closed 필터가 연결되어 있지 않습니다.경로
src/adapters/coding-agent/protocol.ts175/251 - text_delta 를 신뢰하고 그대로 emit 합니다. codebuddy/qoder alike “tools disabled” 라우트에서는 여기가 유출 지점입니다. 공유 파서에 가드를 넣을지, 어댑터별 래퍼로 둘지는 설계 선택입니다.경로
src/adapters/qoder/scaffold-guard.ts-#4190용 마커(<functions.,<invoke, system-reminder) 중심입니다. 이 이슈 로그의 DSML 철자(DSML/functions.exec형태)가 그 마커 집합에 그대로 걸리는지 확인이 필요합니다. 그대로면 이식만으로 되고, 아니면 시그니처를 늘리거나 codebuddy 전용 가드가 필요합니다.경로 재현 로그 - 인터미턴트라고 적혀 있지만 연속 두 턴에서 재현됐습니다. 회귀 테스트는 qoder 가드 테스트 패턴을 복제해 codebuddy 스폰/스트림 fixture 에 마크업 샘플을 넣는 쪽이 안전합니다.
메인테이너의 판단이 필요한 지점
scaffold-guard를 codebuddy 에 복사·연결할지, coding-agent 공유 레이어로 승격할지너의 추천
이슈는 열어 두고, 첫 PR 목표는 qoder 와 같은 fail-closed 가드를 codebuddy outbound 에 연결하는 쪽으로 잡으세요. 먼저 로그 샘플이 기존
scaffold-guard마커에 ヒット하는지 확인하고, 안 되면 DSML 시그니처만 최소 추가하세요. 공유 protocol.ts 개편은 범위가 커지니 1차에서는 어댑터 래퍼로 막고, 형제 어댑터(같은 runCodingAgentTurn) 점검 목록을 follow-up 으로 남기면 됩니다. types/config 분할과 무관하므로 close-don't-rebase 대상이 아닙니다.이 댓글은 grok-bot이 작성했습니다
xiaoshuai7038 commentedon Sep 14, 2026
Verification: the shipped qoder markers do not catch this leak
The review asked whether the DSML spelling in this report is caught by the existing
UNREPAIRABLE_MARKERS, or whether the signature set needs extending. I ran it against the two samples in the issue body, on the published 2.54.0 package, and the answer is that they all miss.Shipped marker set (
src/adapters/qoder/scaffold-guard.ts):The leaked opener decodes to, code point by code point:
So the byte sequence is < U+FF5C U+FF5C DSML U+FF5C U+FF5C space invoke. A literal search for <invoke name= never matches, because the bars and the tag-name separator sit between the angle bracket and "invoke". <functions. never matches either - the only "functions." in the sample is inside the attribute value. All five markers miss on both samples.
Consequence: copying
ScaffoldGuardFilterto codebuddy as-is would not fix this leak. The signature set has to grow first.Signature that does catch it, tested against both samples:
A tolerant form that catches either bar style would be more durable if other DeepSeek surfaces spell it with ASCII pipes; the two samples here do not.
I have also corrected the issue body, which previously claimed these samples would already trip
UNREPAIRABLE_MARKERS. That claim was wrong.Ingwannu commentedon Sep 14, 2026
Confirmed on current dev
4f788f916: the CodeBuddy route still forwards the shared coding-agent text events without a vendor-scaffolding guard, and the existing Qoder marker set does not match the full-width-bar DSML opener in this report.I am taking the narrow fail-closed route for the first fix: a CodeBuddy-owned streaming guard around its outbound text/reasoning channel, matching the observed
<||DSML||and unmatched closing form across chunk boundaries. It will preserve text proven to precede an opening marker, emit one non-retryable upstream error without echoing the command or markup, and suppress the vendor success terminal. I will add adapter-level regressions for full-message and split-delta forms and document that CodeBuddy remains a tools-disabled text/reasoning surface.This does not promote text into executable tool calls and does not change the shared coding-agent parser or Qoder's existing marker contract.
Ingwannu commentedon Sep 14, 2026
The narrow fix is now in draft PR #4627 at
68f0d94dfbe7ce0649813750dc9333ed147e5f04.It recognizes the observed full-width-bar DSML tag across stream-delta boundaries on CodeBuddy Global/CN, refuses once without echoing the embedded command, preserves only text proven to precede the opening tag, and never promotes assistant prose into execution. Focused tests, typecheck, structure/privacy checks and the documentation build pass. The PR remains draft until its exact-head repository CI settles; nothing has been merged and the issue stays open.
Ingwannu commentedon Sep 14, 2026
Update: PR #4627 is now ready for review after all exact-head hosted checks passed on
68f0d94dfbe7ce0649813750dc9333ed147e5f04. @lidge-jun has been requested as the independent maintainer reviewer. The issue remains open until merge/release evidence exists.fix(codebuddy): refuse leaked vendor tool-call scaffolding (#4596)
fix(codebuddy): refuse leaked vendor tool-call scaffolding (#4596)
fix(codebuddy): refuse leaked vendor tool-call scaffolding (#4596)
fix(codebuddy): refuse leaked vendor tool-call scaffolding (#4596)
fix(codebuddy): refuse leaked vendor tool-call scaffolding (#4596)
Merge pull request #4776 from lidge-jun/codex/pw6-codebuddy-scaffold-…
lidge-jun commentedon Sep 16, 2026
Landed via #4776 at 2e590fe
lidge-jun commentedon Sep 16, 2026
Landed via #4776 at 2e590fe
xiaoshuai7038 commentedon Sep 17, 2026
For the record, in case anyone reopens this: the leak still reproduces on the released 2.57.0.
The guard from #4776 only refuses when the invoke target carries the
functions.namespace prefix (DSML_INVOKE_PREFIXinsrc/adapters/codebuddy/scaffold-guard.ts). The model on this route emits the bare tool name - the run I captured choseBash- so the whole block is still forwarded as assistant text. Two attempts out of two, HTTP 200, no error code, guard never fired.Raw response, the exact request, and a per-tool-name table of what the shipped filter accepts versus misses are in #4852.
I tried to reopen this issue rather than open a second one, but GitHub refused the state change for me (the reporter account has no triage access here), so the follow-up carries the evidence instead.
fix(codebuddy): refuse leaked vendor tool-call scaffolding (lidge-jun…
Merge pull request lidge-jun#4776 from lidge-jun/codex/pw6-codebuddy-…