Skip to content
Open
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
7 changes: 7 additions & 0 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ bun run prepare-widget
bunx tauri build --ci --bundles app,dmg
```

Without the signing key, disable updater artifacts or the bundler errors out
(`A public key has been found, but no private key`):

```sh
bunx tauri build --ci --bundles app,dmg --config '{"bundle":{"createUpdaterArtifacts":false}}'
```

Release signing is supplied through environment variables:

```sh
Expand Down
33 changes: 25 additions & 8 deletions src/adapters/codebuddy/scaffold-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,28 @@ interface ScanResult {
lineStart: boolean;
}

// Probe with ASCII-only case folding rather than toLowerCase(): Unicode lowercasing can expand
// a code point (İ, ʼn, ligatures), shifting folded-text offsets away from `text` positions and
// silently disabling detection. `expected` must already be lowercase.
function asciiFold(code: number): number {
return code >= 0x41 && code <= 0x5a ? code + 0x20 : code;
}

function startsWithFolded(text: string, index: number, expected: string): boolean {
if (index + expected.length > text.length) return false;
for (let i = 0; i < expected.length; i++) {
if (asciiFold(text.charCodeAt(index + i)) !== expected.charCodeAt(i)) return false;
}
return true;
}

function prefixAtEnd(text: string, at: number, expected: string): boolean {
const rest = text.slice(at).toLowerCase();
return rest.length < expected.length && expected.startsWith(rest);
const remaining = text.length - at;
if (remaining > expected.length) return false;
for (let i = 0; i < remaining; i++) {
if (asciiFold(text.charCodeAt(at + i)) !== expected.charCodeAt(i)) return false;
}
return true;
}

/**
Expand Down Expand Up @@ -65,8 +84,7 @@ function scan(
}

if (!fence) {
const lowered = text.slice(index).toLowerCase();
if (lowered.startsWith(DSML_CALLS_LINE)) {
if (startsWithFolded(text, index, DSML_CALLS_LINE)) {
const afterCalls = index + DSML_CALLS_LINE.length;
let invokeAt = -1;
if (text[afterCalls] === "\n") invokeAt = afterCalls + 1;
Expand All @@ -76,12 +94,11 @@ function scan(
}

if (invokeAt >= 0) {
const invokeRest = text.slice(invokeAt).toLowerCase();
const invokeNameStart = invokeRest[DSML_INVOKE_PREFIX.length];
if (invokeRest.startsWith(DSML_INVOKE_PREFIX) && invokeNameStart && !/[\s"]/.test(invokeNameStart)) {
const invokeNameStart = text[invokeAt + DSML_INVOKE_PREFIX.length];
if (startsWithFolded(text, invokeAt, 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)) {
if (invokeAt === text.length || prefixAtEnd(text, invokeAt, DSML_INVOKE_PREFIX)) {
return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
}
}
Expand Down
33 changes: 33 additions & 0 deletions tests/providers/codebuddy-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,39 @@ describe("codebuddy runTurn streams a headless turn", () => {
]);
});

test("scans a large multiline delta in time linear in its length", () => {
const run = (lines: number): { events: AdapterEvent[]; elapsed: number } => {
const events: AdapterEvent[] = [];
const guarded = guardCodeBuddyScaffolding(event => events.push(event));
const answer = "a\n".repeat(lines);
const startedAt = performance.now();
guarded({ type: "text_delta", text: answer });
return { events, elapsed: performance.now() - startedAt };
};

const baseline = run(40_000);
const scaled = run(160_000);

// Four times the input must stay near 4x cost; a scan re-walking its suffix could not fit.
expect(scaled.elapsed).toBeLessThan(Math.max(baseline.elapsed * 8, 250));
expect(baseline.events).toEqual([{ type: "text_delta", text: "a\n".repeat(40_000) }]);
expect(scaled.events).toEqual([{ type: "text_delta", text: "a\n".repeat(160_000) }]);
});

test("still refuses scaffolding after a code point that expands when lowercased", () => {
const events: AdapterEvent[] = [];
const guarded = guardCodeBuddyScaffolding(event => events.push(event));

// İ (U+0130) lowercases to two code units, so folded-text offsets no longer match `text`.
guarded({
type: "text_delta",
text: "note İ here\n<||DSML|| calls>\n<||DSML|| invoke name=\"exec\">private-body",
});

expect(events.at(-1)).toEqual(expect.objectContaining({ type: "error", code: "vendor_scaffold_detected" }));
expect(JSON.stringify(events)).not.toContain("private-body");
});

test("delivers quoted and inline-code DSML literals unchanged", () => {
const events: AdapterEvent[] = [];
const guarded = guardCodeBuddyScaffolding(event => events.push(event));
Expand Down
Loading