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:15 -05:00
1 parent 25c12848a2
commit 03542b0845
9 files changed
+87 -6

No files matched your search

@@ -1,5 +1,6 @@
import {
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
@@ -12,6 +13,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",
@@ -112,11 +114,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);
@@ -218,6 +220,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();
@@ -767,6 +767,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";