Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/adapters/codebuddy/scaffold-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ export const CODEBUDDY_SCAFFOLD_ERROR_CODE = "vendor_scaffold_detected";

// The observed control protocol uses FULLWIDTH VERTICAL LINE (U+FF5C). Detection stays
// deliberately narrower than the marker spelling: a calls control line must be followed by an
// invoke line for a functions.* tool. That distinguishes an agent scaffold from prose quoting or
// invoke line with a non-empty tool name. That distinguishes an agent scaffold from prose quoting or
// discussing one tag.
const DSML_CALLS_LINE = "<||dsml|| calls>";
const DSML_INVOKE_PREFIX = "<||dsml|| invoke name=\"functions.";
const DSML_INVOKE_PREFIX = "<||dsml|| invoke name=\"";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update every mapped adapter document

This changes src/adapters/, but the commit updates only structure/providers/chat-compat.md; the source map also lists runtime.md, transports/byte-accounting.md, transports/responses.md, transports/inventory.md, data-planes/inbound-compat.md, providers/cursor.md, and adapters/registry.md. Update every mapped document in this change as required, so the architecture set does not describe the adapter area inconsistently.

AGENTS.md reference: src/AGENTS.md:L11-L11

Useful? React with 👍 / 👎.


export interface CodeBuddyScaffoldFilterResult {
/** Bytes released from a suffix withheld by an earlier event on this channel. */
Expand Down Expand Up @@ -39,7 +39,7 @@ function prefixAtEnd(text: string, at: number, expected: string): boolean {
* Control tags are recognized only at column zero and outside fenced Markdown. Inline code,
* quoted strings, blockquotes, indented source, and prose all add syntax before the tag and are
* therefore forwarded unchanged. A calls line alone is harmless; refusal requires the observed
* two-line calls-plus-functions-invoke grammar.
* two-line calls-plus-named-invoke grammar.
*/
function scan(
text: string,
Expand Down Expand Up @@ -77,7 +77,8 @@ function scan(

if (invokeAt >= 0) {
const invokeRest = text.slice(invokeAt).toLowerCase();
if (invokeRest.startsWith(DSML_INVOKE_PREFIX)) {
const invokeNameStart = invokeRest[DSML_INVOKE_PREFIX.length];
if (invokeRest.startsWith(DSML_INVOKE_PREFIX) && invokeNameStart && !/[\s"]/.test(invokeNameStart)) {
return { safe: text.slice(0, index), held: "", fail: true, fence, lineStart };
}
if (invokeRest.length === 0 || DSML_INVOKE_PREFIX.startsWith(invokeRest)) {
Expand Down
2 changes: 1 addition & 1 deletion structure/providers/chat-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ real image blocks rather than flattening them to the text `[image]`, and orders
blocks chronologically — history before current — so attachment order matches the
prose the model reads beside them. Vendor tool execution stays disabled on both
adapters. CodeBuddy refuses an unquoted, line-oriented full-width-bar DSML `calls`
container followed by a `functions.*` invoke control line in either output channel; it
container followed by a named bare or namespaced invoke control line in either output channel; it
preserves preceding answer text, never promotes vendor prose into execution authority,
and leaves discussed or quoted literals and code examples untouched. Qoder's explicit
refusal of original images is unchanged.
Expand Down
43 changes: 40 additions & 3 deletions tests/providers/codebuddy-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,29 @@ describe("codebuddy runTurn streams a headless turn", () => {
expect(JSON.stringify(events)).not.toContain("secret-command");
});

test.each(["Bash", "exec", "shell", "apply_patch"])("refuses a bare %s DSML invoke", name => {
const events: AdapterEvent[] = [];
const guarded = guardCodeBuddyScaffolding(event => events.push(event));

guarded({
type: "text_delta",
text: `<||DSML|| calls>\n<||DSML|| invoke name="${name}">private-body`,
});

expect(events).toEqual([expect.objectContaining({ type: "error", code: "vendor_scaffold_detected" })]);
});

test("holds a bare invoke prefix split across deltas until its name arrives", () => {
const events: AdapterEvent[] = [];
const guarded = guardCodeBuddyScaffolding(event => events.push(event));

guarded({ type: "text_delta", text: "<||DSML|| calls>\n<||DSML|| invoke name=\"" });
expect(events).toEqual([]);
guarded({ type: "text_delta", text: "Bash\">private-body" });

expect(events).toEqual([expect.objectContaining({ type: "error", code: "vendor_scaffold_detected" })]);
});

test("detects a DSML control sequence split across streamed text deltas", async () => {
const frame = (text: string) => `${JSON.stringify({
type: "stream_event",
Expand Down Expand Up @@ -327,9 +350,9 @@ describe("codebuddy runTurn streams a headless turn", () => {
const events: AdapterEvent[] = [];
const guarded = guardCodeBuddyScaffolding(event => events.push(event));
const answer = "\"<||DSML|| calls>\"\n"
+ "\"<||DSML|| invoke name=\\\"functions.exec\\\">\"\n"
+ "\"<||DSML|| invoke name=\\\"Bash\\\">\"\n"
+ "Use `<||DSML|| calls>` when discussing the literal.\n"
+ "> <||DSML|| calls>\n> <||DSML|| invoke name=\"functions.exec\">";
+ "> <||DSML|| calls>\n> <||DSML|| invoke name=\"exec\">";

guarded({ type: "text_delta", text: answer });
guarded({ type: "done", stopReason: "stop" });
Expand All @@ -344,7 +367,7 @@ describe("codebuddy runTurn streams a headless turn", () => {
const events: AdapterEvent[] = [];
const guarded = guardCodeBuddyScaffolding(event => events.push(event));
const first = "```text\n<||DSML|| calls>\n";
const second = "<||DSML|| invoke name=\"functions.exec\">\n```";
const second = "<||DSML|| invoke name=\"Bash\">\n```";

guarded({ type: "text_delta", text: first });
guarded({ type: "text_delta", text: second });
Expand Down Expand Up @@ -400,6 +423,20 @@ describe("codebuddy runTurn streams a headless turn", () => {
]);
});

test("delivers a calls block whose invoke name is empty", () => {
const events: AdapterEvent[] = [];
const guarded = guardCodeBuddyScaffolding(event => events.push(event));
const answer = "<||DSML|| calls>\n<||DSML|| invoke name=\"\">";

guarded({ type: "text_delta", text: answer });
guarded({ type: "done", stopReason: "stop" });

expect(events).toEqual([
{ type: "text_delta", text: answer },
{ type: "done", stopReason: "stop" },
]);
});

test("queues later events behind an unresolved marker prefix", () => {
const events: AdapterEvent[] = [];
const guarded = guardCodeBuddyScaffolding(event => events.push(event));
Expand Down
Loading