Skip to content

[codebuddy] Routed tool-call markup reaches the client as assistant text (no scaffolding guard, unlike #4190) #4596

Closed

Description

@xiaoshuai7038

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 - the text_delta emit on the full-message path
  • src/adapters/coding-agent/protocol.ts:251 - the same emit inside the content_block_delta branch

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

  1. Configure a provider that uses adapter codebuddy (here: codebuddy-cn).
  2. Route an agent-style Codex turn through it - e.g. ask Codex to inspect git state and read named files in a local repo.
  3. 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 the UNREPAIRABLE_MARKERS shipped 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 usage rows for the same window report provider codebuddy-cn, model deepseek-v4.1-flash, status 200, inbound protocol responses.
  • 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.ts builds 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.ts filters frame types but never inspects frame contents before emitting text_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, role assistant, 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

  1. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  2. added
    bugSomething isn't working
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Sep 14, 2026
  3. lidge-jun commented on Sep 14, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 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.ts 175/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 에 마크업 샘플을 넣는 쪽이 안전합니다.

    메인테이너의 판단이 필요한 지점

    • qoder scaffold-guard 를 codebuddy 에 복사·연결할지, coding-agent 공유 레이어로 승격할지
    • 이 이슈의 DSML 마크업이 기존 UNREPAIRABLE_MARKERS 에 잡히는지, 시그니처 확장이 필요한지
    • “마크업을 진짜 tool_use 로 승격” vs “fail-closed” 중 무엇을 기본으로 할지 (qoder 는 fail-closed; 어댑터 주석의 control-protocol bridge 는 fast-follow)
    • help wanted / needs-design 라벨을 붙일지, qoder 이식 PR 을 바로 받을지

    너의 추천

    이슈는 열어 두고, 첫 PR 목표는 qoder 와 같은 fail-closed 가드를 codebuddy outbound 에 연결하는 쪽으로 잡으세요. 먼저 로그 샘플이 기존 scaffold-guard 마커에 ヒット하는지 확인하고, 안 되면 DSML 시그니처만 최소 추가하세요. 공유 protocol.ts 개편은 범위가 커지니 1차에서는 어댑터 래퍼로 막고, 형제 어댑터(같은 runCodingAgentTurn) 점검 목록을 follow-up 으로 남기면 됩니다. types/config 분할과 무관하므로 close-don't-rebase 대상이 아닙니다.

    이 댓글은 grok-bot이 작성했습니다

  4. xiaoshuai7038 commented on Sep 14, 2026

    @xiaoshuai7038
    Author

    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):

    ["<functions.", "<invoke name=", "<invoke>", "</invoke>", "</system-reminder>"]

    The leaked opener decodes to, code point by code point:

    U+003C  <
    U+FF5C  |
    U+FF5C  |
    U+0044  D
    U+0053  S
    U+004D  M
    U+004C  L
    U+FF5C  |
    U+FF5C  |
    U+0020  space
    U+0069  i   (start of "invoke")
    

    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 ScaffoldGuardFilter to codebuddy as-is would not fix this leak. The signature set has to grow first.

    Signature that does catch it, tested against both samples:

    candidate matches
    U+FF5C U+FF5C + "DSML" both samples, 6 occurrences each
    U+FF5C + "DSML" + U+FF5C both samples, 6 occurrences each
    bare "DSML" word both samples, but too weak to ship - this thread alone contains that word many times
    ASCII-pipe "

    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.

  5. Ingwannu commented on Sep 14, 2026

    @Ingwannu
    Owner

    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.

  6. Ingwannu commented on Sep 14, 2026

    @Ingwannu
    Owner

    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.

  7. Ingwannu commented on Sep 14, 2026

    @Ingwannu
    Owner

    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.

  8. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    Owner

    Landed via #4776 at 2e590fe

  9. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    Owner

    Landed via #4776 at 2e590fe

  10. xiaoshuai7038 commented on Sep 17, 2026

    @xiaoshuai7038
    Author

    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_PREFIX in src/adapters/codebuddy/scaffold-guard.ts). The model on this route emits the bare tool name - the run I captured chose Bash - 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinglanded-via-maintainerOriginal PR closed after landing via a maintainer merge trainproviderProvider adapters, OpenAI-compat presets, upstream API quirkstoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions