From ea5c816e654269ac4815631f9ed3f913d02d90a9 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 21 Jul 2026 21:19:27 +0000 Subject: [PATCH 1/2] fix(write): rejected unknown uri-like targets - Blocked malformed and unregistered URI-like paths before filesystem resolution. - Suggested canonical xd:// spelling while preserving explicitly escaped local paths. - Added regression coverage for xdt://, xd:/, and xd/ near misses. Fixes #6123 --- packages/coding-agent/CHANGELOG.md | 4 +++ .../src/prompts/system/workflow-notice.md | 6 +--- packages/coding-agent/src/tools/write.ts | 28 +++++++++++++++++++ .../test/write-xdev-dispatch.test.ts | 26 +++++++++++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6f32dc885..8d7f1b0ce 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed near-miss `xd://` write targets silently creating filesystem paths instead of surfacing a corrective URI error ([#6123](https://github.com/can1357/oh-my-pi/issues/6123)). + ## [17.0.5] - 2026-07-18 ### Added diff --git a/packages/coding-agent/src/prompts/system/workflow-notice.md b/packages/coding-agent/src/prompts/system/workflow-notice.md index d15ad9127..09a849bef 100644 --- a/packages/coding-agent/src/prompts/system/workflow-notice.md +++ b/packages/coding-agent/src/prompts/system/workflow-notice.md @@ -47,7 +47,7 @@ For independent per-item chains (review → verify, fetch → extract → score) schema: FINDINGS_SCHEMA, }); return await parallel(found.findings.map((f) => async () => ({ - ...f, + …f, verdict: await agent( `Refute if you can (default refuted when unsure): ${f.title}`, { label: `verify:${f.file}`, schema: VERDICT_SCHEMA }, @@ -57,8 +57,6 @@ For independent per-item chains (review → verify, fetch → extract → score) phase("Review"); const results = await parallel(DIMENSIONS.map((d) => async () => reviewAndVerify(d))); const confirmed = results.flat().filter((f) => f.verdict.is_real); - - Reach for `pipeline()` only when a stage genuinely needs ALL of the previous stage first — dedup/merge across the whole set, early-exit on zero, or "compare against the other findings" — because its inter-stage barrier makes every item wait for the slowest peer: **Python (`eval`, Python backend):** @@ -80,8 +78,6 @@ Reach for `pipeline()` only when a stage genuinely needs ALL of the previous sta const verdicts = await parallel(findings.map((f) => async () => await agent(verifyPrompt(f), { schema: VERDICT_SCHEMA }), )); - - Use ordinary code between calls to flatten/map/filter; don't add a barrier just for that. Nested `parallel()` pools each cap independently, so keep total fan-out sane. diff --git a/packages/coding-agent/src/tools/write.ts b/packages/coding-agent/src/tools/write.ts index f1bb1bcd3..e4391bb2b 100644 --- a/packages/coding-agent/src/tools/write.ts +++ b/packages/coding-agent/src/tools/write.ts @@ -85,6 +85,33 @@ import { renderXdevCall, renderXdevResult, type XdevDispatch } from "./xdev"; const LOOSE_HASHLINE_HEADER_RE = /^\s*\[[^#\r\n]+#[^ \t\r\n]*\]\s*$/; const EXECUTABLE_NOTICE = "[Notice: Made executable via chmod +x]"; +const URI_LIKE_WRITE_PATH_RE = /^([a-z][a-z0-9+.-]*):\/{1,2}(.*)$/i; +const XD_MISSING_DELIMITER_RE = /^xd\/+(.*)$/i; +const XD_SCHEME_NEAR_MISSES: Record = { dx: true, xdd: true, xdt: true }; + +function assertWriteTargetAddressable(target: string, router: InternalUrlRouter): void { + const trimmed = target.trim(); + if (path.win32.isAbsolute(trimmed) || router.canHandle(trimmed)) return; + + const missingDelimiter = trimmed.match(XD_MISSING_DELIMITER_RE); + if (missingDelimiter) { + throw new ToolError( + `Unknown URI-like write target '${trimmed}'. Did you mean 'xd://${missingDelimiter[1]}'? Prefix the path with './' to write it as a filesystem path.`, + ); + } + + const uriLike = trimmed.match(URI_LIKE_WRITE_PATH_RE); + if (!uriLike) return; + + const scheme = uriLike[1]!.toLowerCase(); + const canonicalScheme = router.getHandler(scheme) ? scheme : XD_SCHEME_NEAR_MISSES[scheme] ? "xd" : undefined; + const suggestion = canonicalScheme + ? ` Did you mean '${canonicalScheme}://${uriLike[2]}'?` + : " Tool devices use 'xd://'."; + throw new ToolError( + `Unknown URI-like write target '${trimmed}'.${suggestion} Prefix the path with './' to write it as a filesystem path.`, + ); +} const BULK_DIRECTIVE_RE = /^#?(\d+)\s*[:=]\s*(@ours|@theirs|@base|@both)$/; /** @@ -987,6 +1014,7 @@ export class WriteTool implements AgentTool { } }); + it("rejects near-miss xd addresses before filesystem fallback", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "write-xdev-near-miss-")); + try { + const tools = await createTools(xdevSession(tempDir)); + const write = tools.find(entry => entry.name === "write"); + expect(write).toBeDefined(); + + for (const target of ["xdt://web_search", "xd:/web_search", "xd/web_search"]) { + await expect(write!.execute(`write-${target}`, { path: target, content: "{}" })).rejects.toThrow( + "Did you mean 'xd://web_search'?", + ); + } + expect(await Bun.file(path.join(tempDir, "xdt:/web_search")).exists()).toBe(false); + expect(await Bun.file(path.join(tempDir, "xd/web_search")).exists()).toBe(false); + + const escaped = await write!.execute("write-explicit-path", { + path: "./xd/web_search", + content: "intentional file", + }); + expect(escaped.isError).toBeUndefined(); + expect(await Bun.file(path.join(tempDir, "xd/web_search")).text()).toBe("intentional file"); + } finally { + await removeWithRetries(tempDir); + } + }); + it("resolves function-valued device approvals per payload and fails closed on bad content", async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "write-xdev-approval-")); try { From b2e7e34567e9875c6985b39add4be1ac8e0a44ac Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 21 Jul 2026 22:33:33 +0000 Subject: [PATCH 2/2] fix(write): let conflict:// writes reach the resolver - Exempted the handler-less conflict:// scheme from the URI-like guard so parseConflictUri still splices registered blocks. - Extended the near-miss regression to assert conflict://1 reaches the resolver. Fixes #6123 --- packages/coding-agent/src/tools/write.ts | 3 +++ packages/coding-agent/test/write-xdev-dispatch.test.ts | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/packages/coding-agent/src/tools/write.ts b/packages/coding-agent/src/tools/write.ts index e4391bb2b..c1972faf0 100644 --- a/packages/coding-agent/src/tools/write.ts +++ b/packages/coding-agent/src/tools/write.ts @@ -104,6 +104,9 @@ function assertWriteTargetAddressable(target: string, router: InternalUrlRouter) if (!uriLike) return; const scheme = uriLike[1]!.toLowerCase(); + // conflict:// has no router handler but is spliced downstream by + // parseConflictUri (which emits its own precise id/scope errors); let it pass. + if (scheme === "conflict") return; const canonicalScheme = router.getHandler(scheme) ? scheme : XD_SCHEME_NEAR_MISSES[scheme] ? "xd" : undefined; const suggestion = canonicalScheme ? ` Did you mean '${canonicalScheme}://${uriLike[2]}'?` diff --git a/packages/coding-agent/test/write-xdev-dispatch.test.ts b/packages/coding-agent/test/write-xdev-dispatch.test.ts index 811c1446b..aa8f7539c 100644 --- a/packages/coding-agent/test/write-xdev-dispatch.test.ts +++ b/packages/coding-agent/test/write-xdev-dispatch.test.ts @@ -107,6 +107,12 @@ describe("read and write route xd:// device URLs", () => { }); expect(escaped.isError).toBeUndefined(); expect(await Bun.file(path.join(tempDir, "xd/web_search")).text()).toBe("intentional file"); + + // conflict:// has no router handler but is a documented write scheme — + // the guard must let it reach the conflict resolver, not reject it. + await expect(write!.execute("write-conflict", { path: "conflict://1", content: "x" })).rejects.toThrow( + "Conflict #1 not found", + ); } finally { await removeWithRetries(tempDir); }