Authenticate Boat initial checkouts with run-scoped Git credentials

Pass the existing workspace credential provider through computer realization separately from durable metadata, and supply its URL-scoped helper only to the clone subprocess. Verify authenticated clone behavior and credential non-retention.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 10:16:34 -05:00
1 parent 7315159c3d
commit b52863e616
9 files changed
+87 -6

No files matched your search

@@ -303,6 +303,23 @@ describe("environmentRunOrchestrator — realizeForRun", () => {
expect(mockResolveEnvironmentExecutionTarget).toHaveBeenCalledOnce();
});
it("resolves run-scoped checkout credentials without persisting them in workspace metadata", async () => {
const runtime = makeMockRuntime();
const orchestrator = environmentRunOrchestrator(mockDb, { environmentRuntime: runtime });
const request = mockBuildWorkspaceRealizationRequest.getMockImplementation()!();
request.source.repoUrl = "https://github.com/company/private-repo.git";
mockBuildWorkspaceRealizationRequest.mockReturnValue(request);
const gitAuth = { configArgs: ["-c", "credential.helper="], env: { PAPERCLIP_GIT_TOKEN: "scoped-fixture-token" }, source: "managed_connection" as const, secretName: null };
const resolveGitAuth = vi.fn(async () => gitAuth);
await orchestrator.realizeForRun({ ...makeRealizeInput({ environment: makeEnvironment("computer") }), resolveGitAuth });
expect(resolveGitAuth).toHaveBeenCalledExactlyOnceWith(request.source.repoUrl);
expect(runtime.realizeWorkspace).toHaveBeenCalledWith(expect.objectContaining({ gitAuth }));
const dispatched = vi.mocked(runtime.realizeWorkspace).mock.calls[0]![0];
expect(JSON.stringify(dispatched.workspace)).not.toContain("scoped-fixture-token");
expect(JSON.stringify(mockUpdateLeaseMetadata.mock.calls)).not.toContain("scoped-fixture-token");
expect(JSON.stringify(mockUpdateExecutionWorkspace.mock.calls)).not.toContain("scoped-fixture-token");
});
it("uses an in-place authoritative root on the adapter execution target", async () => {
mockResolveEnvironmentExecutionTarget.mockResolvedValue({
kind: "remote",
@@ -1,5 +1,6 @@
import {
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
@@ -13,6 +14,7 @@ import { spawnSync } from "node:child_process";
import { remoteProgram } from "./remote-program.js";
import { afterEach, describe, it, expect, vi } from "vitest";
import { boatBackend } from "./boat.js";
import { buildGitAuthInvocation } from "../../../services/git-credentials.js";
import type { ComputerRecord } from "../domain/ledger.js";
const record: ComputerRecord = {
id: "computer",
@@ -113,11 +115,11 @@ function fixture() {
const temp = realpathSync(mkdtempSync(join(tmpdir(), "computer-files-")));
roots.push(temp);
const root = join(temp, "home");
const call = (input: Record<string, unknown>) => {
const call = (input: Record<string, unknown>, env = process.env) => {
const result = spawnSync(
"python3",
["-c", remoteProgram.replaceAll("/home/user/paperclip/", `${temp}/`)],
{ input: JSON.stringify({ ...input, root }), encoding: "utf8" },
{ input: JSON.stringify({ ...input, root }), encoding: "utf8", env },
);
if (result.status !== 0) throw new Error(result.stderr);
return JSON.parse(result.stdout);
@@ -238,6 +240,40 @@ describe("confined computer files", () => {
expect(f.call({ action: "list", limit: 2 }).entries).toHaveLength(2);
expect(f.call({ action: "list", limit: 1001 })).toEqual({ error: "invalid" });
});
it("clones a private checkout with operation-scoped auth and leaves no credential material behind", () => {
const f = fixture();
const source = join(f.temp, "source");
mkdirSync(source);
const realGit = spawnSync("which", ["git"], { encoding: "utf8" }).stdout.trim();
for (const args of [["init"], ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "base"]]) {
expect(spawnSync(realGit, args, { cwd: source, encoding: "utf8" }).status).toBe(0);
}
const bin = join(f.temp, "bin");
mkdirSync(bin);
// Stand in for an authenticated GitHub transport, using real Git and the
// production URL-scoped credential helper, without any external network.
writeFileSync(join(bin, "git"), `#!${process.execPath}
const {spawnSync}=require("node:child_process");
const args=process.argv.slice(2), clone=args.indexOf("clone");
if(clone<0) process.exit(9);
if(args.some(arg=>arg.includes("scoped-fixture-token"))) process.exit(10);
const credential=spawnSync(process.env.FIXTURE_REAL_GIT,[...args.slice(0,clone),"credential","fill"],{input:"protocol=https\\nhost=github.com\\n\\n",encoding:"utf8"});
if(credential.status!==0||!credential.stdout.includes("password=scoped-fixture-token")) process.exit(11);
args[clone+2]=process.env.FIXTURE_SOURCE;
const result=spawnSync(process.env.FIXTURE_REAL_GIT,args,{encoding:"utf8"});
process.stdout.write(result.stdout||"");process.stderr.write(result.stderr||"");process.exit(result.status??1);
`, { mode: 0o700 });
const env = { ...process.env, PATH: `${bin}:${process.env.PATH}`, FIXTURE_REAL_GIT: realGit, FIXTURE_SOURCE: source };
const request = { action: "workspace", mode: "shared", repositoryUrl: "https://github.com/company/private.git" };
expect(f.call(request, env)).toEqual({ error: "command_failed" });
const gitAuth = buildGitAuthInvocation({ token: "scoped-fixture-token", source: "managed_connection", secretName: null });
const result = f.call({ ...request, gitAuth }, env);
expect(result.remoteCwd).toBe(join(f.root, "checkout"));
const config = readFileSync(join(result.remoteCwd, ".git", "config"), "utf8");
expect(config).not.toContain("scoped-fixture-token");
expect(config).not.toContain("credential");
expect(spawnSync(realGit, ["rev-parse", "HEAD"], { cwd: result.remoteCwd }).status).toBe(0);
});
it.each([false, true])("uses the requested worktree branch separately from its start ref (explicit base: %s)", explicitBase => {
const f = fixture();
const checkout = join(f.root, "checkout");
@@ -5,8 +5,8 @@ p=json.load(sys.stdin)
def fail(code):
print(json.dumps({'error':code}));sys.exit(0)
def digest(data):return hashlib.sha256(data).hexdigest()
def run(args,cwd=None):
r=subprocess.run(args,cwd=cwd,capture_output=True,text=True)
def run(args,cwd=None,env=None):
r=subprocess.run(args,cwd=cwd,env=env,capture_output=True,text=True)
if r.returncode:fail('command_failed')
return r.stdout.strip()
root=p['root']
@@ -65,7 +65,13 @@ if act=='workspace':
checkout=os.path.join(root,'checkout')
if repo:
if not (repo.startswith('https://') or repo.startswith('ssh://') or repo.startswith('git@')) or '\n' in repo:fail('invalid')
if not os.path.exists(checkout):run(['git','clone','--',repo,checkout])
if not os.path.exists(checkout):
# Resolve credentials on the controller for this operation. Only the clone
# child receives the helper environment; no credential file or Git config persists.
auth=p.get('gitAuth') or {};git_env=dict(os.environ)
git_env.update({'GIT_TERMINAL_PROMPT':'0','GIT_CONFIG_GLOBAL':'/dev/null','GIT_CONFIG_NOSYSTEM':'1'})
git_env.update(auth.get('env',{}))
run(['git']+auth.get('configArgs',[])+['clone','--',repo,checkout],env=git_env)
elif run(['git','remote','get-url','origin'],checkout)!=repo:fail('conflict')
else:
os.makedirs(checkout,exist_ok=True)
@@ -113,6 +113,16 @@ function fixture() {
};
}
describe("computer ownership", () => {
it("passes checkout credentials transiently without retaining them in computer state", async () => {
const f = fixture();
await f.attach();
const binding = await f.admit();
const gitAuth = { configArgs: ["-c", "credential.helper="], env: { PAPERCLIP_GIT_TOKEN: "scoped-fixture-token" } };
await f.service.realizeWorkspace({ ...f.scope, owner: binding.owner, projectId: "project", mode: "shared",
repositoryUrl: "https://github.com/company/private.git", gitAuth });
expect(f.backend.remote).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ action: "workspace", gitAuth }));
expect(JSON.stringify(await f.repository.get(f.scope))).not.toContain("scoped-fixture-token");
});
it("explains an unavailable dev server without masking provider errors", async () => {
const f = fixture();
await f.attach();
@@ -770,6 +770,8 @@ finally:
projectId?: string;
taskId?: string;
repositoryUrl?: string;
/** Transient Git helper configuration; sent over stdin, never retained in the ledger. */
gitAuth?: { configArgs: string[]; env: Record<string, string> };
branch?: string;
baseRef?: string;
mode: "shared" | "worktree";
@@ -77,11 +77,12 @@ export function createComputerEnvironmentDriver(db: Db): EnvironmentRuntimeDrive
if (!result.retired) throw new Error("computer_owner_retirement_superseded");
return { providerLeaseId: lease.providerLeaseId, state: "stopped" };
},
async realizeWorkspace({ lease, workspace }) {
async realizeWorkspace({ lease, workspace, gitAuth }) {
const request = workspace.metadata?.workspaceRealizationRequest as { source?: { projectId?: string; repoUrl?: string; repoRef?: string; branchName?: string; strategy?: string }; issueId?: string } | undefined;
const result = await computers.realizeWorkspace({ ...scope(lease), owner: computerOwnerFromLease(lease),
projectId: request?.source?.projectId ?? undefined, taskId: request?.issueId ?? undefined,
repositoryUrl: request?.source?.repoUrl ?? undefined,
gitAuth,
branch: request?.source?.branchName ?? request?.source?.repoRef ?? undefined,
baseRef: request?.source?.repoRef ?? undefined,
mode: request?.source?.strategy === "git_worktree" ? "worktree" : "shared" });
@@ -51,6 +51,7 @@ import { buildWorkspaceRealizationRequest } from "./workspace-realization.js";
import { executionWorkspaceService } from "./execution-workspaces.js";
import { logActivity } from "./activity-log.js";
import { logger } from "../middleware/logger.js";
import type { GitRemoteAuthProvider } from "./git-credentials.js";
import { parseObject } from "../adapters/utils.js";
import type { RealizedExecutionWorkspace } from "./workspace-runtime.js";
import type { PluginWorkerManager } from "./plugin-worker-manager.js";
@@ -393,6 +394,8 @@ export function environmentRunOrchestrator(
* target. Absent keeps the safe no-op default in the bridge.
*/
duplexObservabilityRecorder?: DuplexObservabilityRecorder | null;
/** Run-scoped auth for initial persistent checkouts; never persisted as workspace metadata. */
resolveGitAuth?: GitRemoteAuthProvider;
}): Promise<EnvironmentRealizationResult> {
const {
environment,
@@ -430,6 +433,9 @@ export function environmentRunOrchestrator(
const workspaceRealizationResult = await environmentRuntime.realizeWorkspace({
environment,
lease,
gitAuth: environment.driver === "computer" && workspaceRealizationRequest.source.repoUrl
? await input.resolveGitAuth?.(workspaceRealizationRequest.source.repoUrl) ?? undefined
: undefined,
workspace: {
localPath: executionWorkspace.cwd,
remotePath: remoteCwd,
@@ -526,6 +526,8 @@ export interface EnvironmentDriverLeaseInput {
}
export interface EnvironmentDriverRealizeWorkspaceInput extends EnvironmentDriverLeaseInput {
/** Ephemeral authentication for the initial checkout, excluded from durable metadata. */
gitAuth?: { configArgs: string[]; env: Record<string, string> };
workspace: {
localPath?: string;
remotePath?: string;
+1
View File
@@ -4529,6 +4529,7 @@ export function heartbeatService(
effectiveExecutionWorkspaceMode,
persistedExecutionWorkspace,
duplexObservabilityRecorder,
resolveGitAuth: workspaceGitAuthProvider,
});
nativeRunnerPreparationSpans.push({
name: "environment.workspace.realize",