Skip to content

Commit 695ec74

Browse files
committed
Fix sandbox mount prompt regex scanning
Change-Id: I3be5fd219283020e88121c24cfa8d888915cd2d3
1 parent 02878f6 commit 695ec74

2 files changed

Lines changed: 74 additions & 10 deletions

File tree

packages/sdk/src/internal/utils/sandbox-mount.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ function quoteShellWord(value: string): string {
2626
return `'${value.replaceAll("'", `'"'"'`)}'`;
2727
}
2828

29+
function stripUrlQueryAndFragment(value: string): string {
30+
const queryIndex = value.indexOf("?");
31+
const fragmentIndex = value.indexOf("#");
32+
if (queryIndex === -1) {
33+
return fragmentIndex === -1 ? value : value.slice(0, fragmentIndex);
34+
}
35+
if (fragmentIndex === -1) return value.slice(0, queryIndex);
36+
return value.slice(0, Math.min(queryIndex, fragmentIndex));
37+
}
38+
39+
function stripGitSuffix(value: string): string {
40+
return value.toLowerCase().endsWith(".git") ? value.slice(0, -4) : value;
41+
}
42+
43+
function lastRemotePathSegment(value: string): string {
44+
const lastSlash = value.lastIndexOf("/");
45+
const lastColon = value.lastIndexOf(":");
46+
return value.slice(Math.max(lastSlash, lastColon) + 1).trim();
47+
}
48+
2949
export function providerMountPrefix(provider: string): string | undefined {
3050
return PROVIDER_MOUNT_PREFIXES[provider];
3151
}
@@ -62,12 +82,7 @@ export function resolveRepositoryMountPath(provider: string, resource: SessionGi
6282
return resource.mount_path;
6383
}
6484
// Accept URL remotes as well as scp-like SSH remotes such as git@host:team/repo.git.
65-
const repositoryName = resource.url
66-
.replace(/[?#].*$/, "")
67-
.split(/[/:]/)
68-
.filter(Boolean)
69-
.at(-1)
70-
?.replace(/\.git$/i, "");
85+
const repositoryName = stripGitSuffix(lastRemotePathSegment(stripUrlQueryAndFragment(resource.url)));
7186
if (!repositoryName) {
7287
throw new UserError(`Cannot derive a ${provider} Git repository mount path from URL '${resource.url}'.`);
7388
}
@@ -95,13 +110,32 @@ export function prependFileHint(prompt: string, files: MountedFile[] | undefined
95110
return `${hint}\n\n${prompt}`;
96111
}
97112

98-
const FILE_MENTION_SENTINEL_RE = /\u27E6file:(.+?)\u27E7/g;
113+
const FILE_MENTION_START = "\u27E6file:";
114+
const FILE_MENTION_END = "\u27E7";
99115

100116
/** 将 prompt 内 ⟦file:mountPath⟧ 占位符替换为 provider 感知的真实 sandbox 路径 */
101117
export function rewriteFileMentions(prompt: string, provider: string): string {
102-
return prompt.replace(FILE_MENTION_SENTINEL_RE, (_match, mountPath: string) =>
103-
resolveSandboxMountPath(provider, mountPath),
104-
);
118+
let cursor = 0;
119+
let rewritten = "";
120+
121+
while (cursor < prompt.length) {
122+
const start = prompt.indexOf(FILE_MENTION_START, cursor);
123+
if (start === -1) {
124+
rewritten += prompt.slice(cursor);
125+
break;
126+
}
127+
const mountPathStart = start + FILE_MENTION_START.length;
128+
const end = prompt.indexOf(FILE_MENTION_END, mountPathStart);
129+
if (end === -1) {
130+
rewritten += prompt.slice(cursor);
131+
break;
132+
}
133+
rewritten += prompt.slice(cursor, start);
134+
rewritten += resolveSandboxMountPath(provider, prompt.slice(mountPathStart, end));
135+
cursor = end + FILE_MENTION_END.length;
136+
}
137+
138+
return rewritten;
105139
}
106140

107141
/** 发送给 provider 前统一处理:先替换 mention 占位符,再 prepend 文件 hint */

packages/sdk/tests/unit/sandbox-mount.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ describe("rewriteFileMentions", () => {
9090
expect(rewriteFileMentions(prompt, "bailian")).toBe("see /mnt/uploads/a.png here");
9191
});
9292

93+
test("rewrites multiple sentinels with a linear scanner", () => {
94+
const prompt = [`first ${"\u27E6file:uploads/a.png\u27E7"}`, `second ${"\u27E6file:uploads/b.png\u27E7"}`].join(
95+
" and ",
96+
);
97+
expect(rewriteFileMentions(prompt, "qoder")).toBe("first /data/uploads/a.png and second /data/uploads/b.png");
98+
});
99+
100+
test("leaves unterminated sentinels unchanged", () => {
101+
const longTail = "x".repeat(10_000);
102+
const prompt = `before ${"\u27E6file:uploads/a.png"}${longTail}`;
103+
expect(rewriteFileMentions(prompt, "bailian")).toBe(prompt);
104+
});
105+
93106
test("no sentinel → unchanged", () => {
94107
expect(rewriteFileMentions("hello", "bailian")).toBe("hello");
95108
});
@@ -125,6 +138,23 @@ describe("prepareInitialSessionPrompt", () => {
125138
expect(out.endsWith("implement the feature")).toBe(true);
126139
});
127140

141+
test("derives repository names without regex query or fragment stripping", () => {
142+
const out = prepareInitialSessionPrompt(
143+
"inspect it",
144+
bindings([
145+
{
146+
type: "github_repository",
147+
url: "https://git.example.com/team/repo.git?token=abc#readme",
148+
authorization_token: "do-not-leak",
149+
},
150+
]),
151+
"qoder",
152+
);
153+
expect(out).toContain("`/data/workspace/repo`");
154+
expect(out).not.toContain("token=abc");
155+
expect(out).not.toContain("readme");
156+
});
157+
128158
test("shell-quotes an explicit repository mount path", () => {
129159
const out = prepareInitialSessionPrompt(
130160
"inspect it",

0 commit comments

Comments
 (0)