From 5608eb2b35ff740520d494155cfc598ffee66e80 Mon Sep 17 00:00:00 2001 From: fy2ne Date: Wed, 16 Sep 2026 13:26:46 -0700 Subject: [PATCH 1/2] feat(github): support blobless clone and configurable clone timeout --- .../src/tools/clone-repository.ts | 37 ++++++++--- .../tests/clone-repository.test.ts | 65 +++++++++++++++++++ 2 files changed, 93 insertions(+), 9 deletions(-) diff --git a/packages/junior-github/src/tools/clone-repository.ts b/packages/junior-github/src/tools/clone-repository.ts index 90af10f6a3..e62999b64a 100644 --- a/packages/junior-github/src/tools/clone-repository.ts +++ b/packages/junior-github/src/tools/clone-repository.ts @@ -34,6 +34,21 @@ const inputSchema = z .describe( "Set true to keep an intentional ad-hoc checkout when matching Workspaces exist. Prefer switchWorkspace; the checkout is already present after a successful switch.", ), + blobless: z + .boolean() + .optional() + .describe( + "Set true to perform a blobless shallow clone (--filter=blob:none) to reduce bandwidth and clone times on large repositories.", + ), + timeoutSeconds: z + .number() + .int() + .positive() + .max(900) + .optional() + .describe( + "Optional timeout in seconds for the clone operation. Defaults to 300 seconds (5 minutes).", + ), }) .strict(); const cloneSchema = z.object({ @@ -206,20 +221,24 @@ export function createGitHubCloneRepositoryTool(ctx: { // ls-remote, so egress policy cannot distinguish clone without also // blocking normal repository workflows. This tool is the bounded, // preferred clone path rather than an enforceable network boundary. + const timeoutMs = (input.timeoutSeconds ?? 300) * 1000; + const cloneArgs = ["clone", "--quiet", "--depth=1"]; + if (input.blobless === true) { + cloneArgs.push("--filter=blob:none"); + } + cloneArgs.push( + "--", + `https://github.com/${repo.owner}/${repo.name}.git`, + directory, + ); + let clone; try { clone = await ctx.sandbox.run({ cmd: "git", - args: [ - "clone", - "--quiet", - "--depth=1", - "--", - `https://github.com/${repo.owner}/${repo.name}.git`, - directory, - ], + args: cloneArgs, cwd: ctx.sandbox.root, - signal: commandSignal(options.signal, 2 * 60_000), + signal: commandSignal(options.signal, timeoutMs), }); } catch (error) { await removePartialClone(ctx, path); diff --git a/packages/junior-github/tests/clone-repository.test.ts b/packages/junior-github/tests/clone-repository.test.ts index f3c8e05abb..ef7628563b 100644 --- a/packages/junior-github/tests/clone-repository.test.ts +++ b/packages/junior-github/tests/clone-repository.test.ts @@ -265,4 +265,69 @@ describe("cloneRepository", () => { ); expect(run).toHaveBeenCalledTimes(3); }); + + it("supports blobless shallow clone with --filter=blob:none", async () => { + const run = vi + .fn() + .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" }) + .mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "" }) + .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" }); + const tool = createGitHubCloneRepositoryTool(context(run)); + + const result = await tool.execute!( + { repo: "getsentry/junior", blobless: true }, + {} as never, + ); + + expect(run).toHaveBeenNthCalledWith(3, { + cmd: "git", + args: [ + "clone", + "--quiet", + "--depth=1", + "--filter=blob:none", + "--", + "https://github.com/getsentry/junior.git", + "repos/junior", + ], + cwd: "/vercel/sandbox", + signal: expect.any(AbortSignal), + }); + expect(result).toMatchObject({ + path: "/vercel/sandbox/repos/junior", + repo: "getsentry/junior", + }); + }); + + it("accepts custom timeoutSeconds for clone operations", async () => { + const run = vi + .fn() + .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" }) + .mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "" }) + .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" }); + const tool = createGitHubCloneRepositoryTool(context(run)); + + const result = await tool.execute!( + { repo: "getsentry/junior", timeoutSeconds: 450 }, + {} as never, + ); + + expect(run).toHaveBeenNthCalledWith(3, { + cmd: "git", + args: [ + "clone", + "--quiet", + "--depth=1", + "--", + "https://github.com/getsentry/junior.git", + "repos/junior", + ], + cwd: "/vercel/sandbox", + signal: expect.any(AbortSignal), + }); + expect(result).toMatchObject({ + path: "/vercel/sandbox/repos/junior", + repo: "getsentry/junior", + }); + }); }); From 3f7f80fca4d5cebb08dd543e9794dfcaad4c6181 Mon Sep 17 00:00:00 2001 From: fy2ne Date: Wed, 16 Sep 2026 13:38:15 -0700 Subject: [PATCH 2/2] fix(github): cap clone timeoutSeconds at 300s (sandbox executor limit) --- packages/junior-github/src/tools/clone-repository.ts | 4 ++-- packages/junior-github/tests/clone-repository.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/junior-github/src/tools/clone-repository.ts b/packages/junior-github/src/tools/clone-repository.ts index e62999b64a..9c83c3bb4d 100644 --- a/packages/junior-github/src/tools/clone-repository.ts +++ b/packages/junior-github/src/tools/clone-repository.ts @@ -44,10 +44,10 @@ const inputSchema = z .number() .int() .positive() - .max(900) + .max(300) .optional() .describe( - "Optional timeout in seconds for the clone operation. Defaults to 300 seconds (5 minutes).", + "Optional timeout in seconds for the clone operation. Defaults to 300 seconds (5 minutes), which is the sandbox executor cap.", ), }) .strict(); diff --git a/packages/junior-github/tests/clone-repository.test.ts b/packages/junior-github/tests/clone-repository.test.ts index ef7628563b..868ba88810 100644 --- a/packages/junior-github/tests/clone-repository.test.ts +++ b/packages/junior-github/tests/clone-repository.test.ts @@ -308,7 +308,7 @@ describe("cloneRepository", () => { const tool = createGitHubCloneRepositoryTool(context(run)); const result = await tool.execute!( - { repo: "getsentry/junior", timeoutSeconds: 450 }, + { repo: "getsentry/junior", timeoutSeconds: 250 }, {} as never, );