From 309aa0f33f33cbd3fa9fc7def4168477d798b925 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 19 Aug 2026 17:16:28 +0000 Subject: [PATCH] Fix fresh workspace ambient credential probing Co-Authored-By: Paperclip --- .../__tests__/heartbeat-project-env.test.ts | 2 +- .../heartbeat-workspace-session.test.ts | 47 +++++++ server/src/services/heartbeat.ts | 125 ++++++++++++++---- 3 files changed, 145 insertions(+), 29 deletions(-) diff --git a/server/src/__tests__/heartbeat-project-env.test.ts b/server/src/__tests__/heartbeat-project-env.test.ts index eaf239f5c5e..613243937bb 100644 --- a/server/src/__tests__/heartbeat-project-env.test.ts +++ b/server/src/__tests__/heartbeat-project-env.test.ts @@ -479,7 +479,7 @@ describe("resolveExecutionRunAdapterConfig", () => { } as any, }); - expect(probe).toHaveBeenCalledWith("/checkout"); + expect(probe).toHaveBeenCalledWith("/checkout", { configuredPushUrls: undefined }); expect(result.resolvedConfig.env).toEqual({ AGENT_ONLY: "agent-only" }); expect(result.secretKeys).toEqual(new Set()); expect(getByNameInsensitive).not.toHaveBeenCalled(); diff --git a/server/src/__tests__/heartbeat-workspace-session.test.ts b/server/src/__tests__/heartbeat-workspace-session.test.ts index dc201887ae1..9b0f1ff063a 100644 --- a/server/src/__tests__/heartbeat-workspace-session.test.ts +++ b/server/src/__tests__/heartbeat-workspace-session.test.ts @@ -24,6 +24,7 @@ import { preflightLowTrustWorkspaceIsolation, probeAmbientHostPushCredential, prioritizeProjectWorkspaceCandidatesForRun, + resolveAmbientHostCredentialCheckout, parseSessionCompactionPolicy, provisionExecutionWorkspaceForFreshnessDecision, reconcileReusedExecutionWorkspaceProjectWorkspaceId, @@ -1017,6 +1018,52 @@ describe("probeAmbientHostPushCredential", () => { execFile: execFileMock as unknown as typeof execFile, })).resolves.toBeNull(); }); + + it("accepts the selected fresh workspace SSH URL before its checkout exists", async () => { + const execFileMock = vi.fn(async (command: string) => { + if (command === "gh") throw Object.assign(new Error("not logged in"), { code: 1 }); + throw Object.assign(new Error("checkout not realized"), { code: "ENOENT" }); + }); + + await expect(probeAmbientHostPushCredential("/future/checkout", { + execFile: execFileMock as unknown as typeof execFile, + configuredPushUrls: ["git@github.com:example/repo.git"], + })).resolves.toBe("ssh_push_remote"); + expect(execFileMock).toHaveBeenCalledTimes(2); + }); +}); + +describe("resolveAmbientHostCredentialCheckout", () => { + it("uses the preferred fresh project workspace instead of the static adapter cwd", () => { + expect(resolveAmbientHostCredentialCheckout({ + adapterCwd: "/static/adapter", + preferredProjectWorkspaceId: "workspace-2", + projectWorkspaces: [ + { id: "workspace-1", cwd: "/checkout/one", repoUrl: "https://github.com/example/one.git" }, + { id: "workspace-2", cwd: "/__paperclip_repo_only__", repoUrl: "git@github.com:example/two.git" }, + ], + })).toEqual({ + checkoutCwd: "/static/adapter", + configuredPushUrls: ["git@github.com:example/two.git"], + }); + }); + + it("prefers a reusable realized checkout over project and adapter paths", () => { + expect(resolveAmbientHostCredentialCheckout({ + adapterCwd: "/static/adapter", + preferredProjectWorkspaceId: "workspace-1", + projectWorkspaces: [ + { id: "workspace-1", cwd: "/project/checkout", repoUrl: "git@github.com:example/project.git" }, + ], + reusableExecutionWorkspace: { + cwd: "/reused/checkout", + repoUrl: "ssh://git@github.com/example/reused.git", + }, + })).toEqual({ + checkoutCwd: "/reused/checkout", + configuredPushUrls: ["ssh://git@github.com/example/reused.git"], + }); + }); }); describe("shouldProbeAmbientHostPushCredential", () => { diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index d9c3d424a7f..0767a307fe2 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -855,10 +855,14 @@ export type AmbientHostPushCredential = "gh_cli" | "ssh_push_remote"; */ export async function probeAmbientHostPushCredential( cwd: string | null | undefined, - deps: { execFile: typeof execFile } = { execFile }, + deps: { + execFile?: typeof execFile; + configuredPushUrls?: readonly string[]; + } = {}, ): Promise { + const execFileImpl = deps.execFile ?? execFile; try { - const result = await deps.execFile( + const result = await execFileImpl( "gh", ["auth", "token", "--hostname", "github.com"], { timeout: AMBIENT_GITHUB_CREDENTIAL_PROBE_TIMEOUT_MS, windowsHide: true }, @@ -868,40 +872,75 @@ export async function probeAmbientHostPushCredential( // A missing CLI, timeout, non-zero exit, or unreadable auth state is a tier miss. } + const pushUrls: string[] = []; const normalizedCwd = readNonEmptyString(cwd); - if (!normalizedCwd) return null; - const remoteNames = await deps.execFile( - "git", - ["remote"], - { - cwd: normalizedCwd, - timeout: AMBIENT_GITHUB_CREDENTIAL_PROBE_TIMEOUT_MS, - windowsHide: true, - }, - ).then((result) => - result.stdout - .split(/\r?\n/) - .map((value) => value.trim()) - .filter((value) => value.length > 0), - ).catch(() => []); - - for (const remoteName of remoteNames) { - const pushUrl = await deps.execFile( + if (normalizedCwd) { + const remoteNames = await execFileImpl( "git", - ["remote", "get-url", "--push", remoteName], + ["remote"], { cwd: normalizedCwd, timeout: AMBIENT_GITHUB_CREDENTIAL_PROBE_TIMEOUT_MS, windowsHide: true, }, - ).then((result) => readNonEmptyString(result.stdout)).catch(() => null); - if (pushUrl && (/^ssh:\/\//i.test(pushUrl) || /^[^\s/@:]+@[^\s/:]+:.+/.test(pushUrl))) { - return "ssh_push_remote"; + ).then((result) => + result.stdout + .split(/\r?\n/) + .map((value) => value.trim()) + .filter((value) => value.length > 0), + ).catch(() => []); + + for (const remoteName of remoteNames) { + const pushUrl = await execFileImpl( + "git", + ["remote", "get-url", "--push", remoteName], + { + cwd: normalizedCwd, + timeout: AMBIENT_GITHUB_CREDENTIAL_PROBE_TIMEOUT_MS, + windowsHide: true, + }, + ).then((result) => readNonEmptyString(result.stdout)).catch(() => null); + if (pushUrl) pushUrls.push(pushUrl); } } + + // A fresh workspace has no checkout to inspect yet. Its selected project-workspace URL becomes + // the clone's origin, so treat that URL as the future push remote. This remains deliberately + // network-free: the tier identifies SSH transport, while the eventual push remains the authority + // for key, agent, host, and repository authorization. + pushUrls.push(...(deps.configuredPushUrls ?? [])); + if (pushUrls.some((pushUrl) => ( + /^ssh:\/\//i.test(pushUrl) || /^[^\s/@:]+@[^\s/:]+:.+/.test(pushUrl) + ))) { + return "ssh_push_remote"; + } return null; } +export function resolveAmbientHostCredentialCheckout(input: { + adapterCwd: string | null | undefined; + preferredProjectWorkspaceId: string | null | undefined; + projectWorkspaces: Array<{ id: string; cwd: string | null; repoUrl: string | null }>; + reusableExecutionWorkspace?: { cwd: string | null; repoUrl: string | null } | null; +}) { + const selectedProjectWorkspace = prioritizeProjectWorkspaceCandidatesForRun( + input.projectWorkspaces, + input.preferredProjectWorkspaceId, + )[0] ?? null; + const reusableCwd = readNonEmptyString(input.reusableExecutionWorkspace?.cwd); + const projectCwd = readNonEmptyString(selectedProjectWorkspace?.cwd); + const checkoutCwd = reusableCwd + ?? (projectCwd === REPO_ONLY_CWD_SENTINEL ? null : projectCwd) + ?? readNonEmptyString(input.adapterCwd); + const configuredPushUrl = readNonEmptyString( + input.reusableExecutionWorkspace?.repoUrl ?? selectedProjectWorkspace?.repoUrl, + ); + return { + checkoutCwd, + configuredPushUrls: configuredPushUrl ? [configuredPushUrl] : [], + }; +} + export function shouldProbeAmbientHostPushCredential(input: { pushCapabilityPreflightRequired: boolean; environmentDriver: string | null | undefined; @@ -996,6 +1035,7 @@ export async function resolveExecutionRunAdapterConfig(input: { ambientHostCredential?: { enabled: boolean; checkoutCwd?: string | null; + configuredPushUrls?: readonly string[]; probe?: typeof probeAmbientHostPushCredential; }; }; @@ -1050,7 +1090,10 @@ export async function resolveExecutionRunAdapterConfig(input: { && requiredScopedEnvBinding.ambientHostCredential?.enabled ? await ( requiredScopedEnvBinding.ambientHostCredential.probe ?? probeAmbientHostPushCredential - )(requiredScopedEnvBinding.ambientHostCredential.checkoutCwd).catch(() => null) + )( + requiredScopedEnvBinding.ambientHostCredential.checkoutCwd, + { configuredPushUrls: requiredScopedEnvBinding.ambientHostCredential.configuredPushUrls }, + ).catch(() => null) : null; let fallbackSecret: Awaited> | null = null; let fallbackAuthorization: { @@ -14794,6 +14837,33 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) environmentDriver: selectedEnvironmentForConfig?.driver ?? null, trustPreset, }); + const ambientProjectWorkspaceRows = ambientHostPushCredentialEnabled + && requestedExecutionWorkspaceMode !== "agent_default" + && projectContext?.id + ? await db + .select({ + id: projectWorkspaces.id, + cwd: projectWorkspaces.cwd, + repoUrl: projectWorkspaces.repoUrl, + }) + .from(projectWorkspaces) + .where(and( + eq(projectWorkspaces.companyId, agent.companyId), + eq(projectWorkspaces.projectId, projectContext.id), + )) + .orderBy(asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id)) + : []; + const ambientHostCredentialCheckout = resolveAmbientHostCredentialCheckout({ + adapterCwd: executionRunConfig.cwd as string | null | undefined, + preferredProjectWorkspaceId: issueRef?.projectWorkspaceId, + projectWorkspaces: ambientProjectWorkspaceRows, + reusableExecutionWorkspace: reusableExistingExecutionWorkspace + ? { + cwd: reusableExistingExecutionWorkspace.cwd, + repoUrl: reusableExistingExecutionWorkspace.repoUrl, + } + : null, + }); const { resolvedConfig, secretKeys, secretManifest } = await resolveExecutionRunAdapterConfig({ companyId: agent.companyId, agentId: agent.id, @@ -14823,9 +14893,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) fallbackInjectionEnvKey: "GH_TOKEN", ambientHostCredential: { enabled: ambientHostPushCredentialEnabled, - checkoutCwd: - reusableExistingExecutionWorkspace?.cwd - ?? executionRunConfig.cwd as string | null | undefined, + checkoutCwd: ambientHostCredentialCheckout.checkoutCwd, + configuredPushUrls: ambientHostCredentialCheckout.configuredPushUrls, }, } : undefined,