Exclude private nested repositories before checkpoint path validation

Reproduce Codex plugin-cache repositories with real Git and preserve private-runtime exclusions before validating directory entries.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-08 17:24:04 -05:00
parent 36f8d69931
commit 97bf34eb60
3 changed files with 22 additions and 4 deletions

View File

@ -47,9 +47,9 @@ Sandbox runs use the operating-system user's home directory. Both legacy
adapters and the native runner enter the same host-owned lifecycle before
dispatch. Local execution keeps its existing workspace and home behavior.
Legacy ACP proxies use a private host staging directory while agent sessions start
in the sandbox home. When managed Git launchers are present, the remote agent
starts without another login-shell profile pass, which could replace the assigned
PATH before the agent starts. For API-key Codex ACP runs, the adapter writes the explicit
in the sandbox home. After login-shell initialization, remote agents restore the
managed Git PATH ahead of paths added by the profile. This preserves both the
Git launcher and custom runtime initialization. For API-key Codex ACP runs, the adapter writes the explicit
key to an owner-only login file in the staging copy; host credentials stay unchanged.
Per-run GitHub launchers declare their own CommonJS package scope so warm runs
inside ES-module repositories can still execute Git and GitHub CLI commands. Native runner launches carry the validated scoped paths
@ -158,7 +158,8 @@ warm checkout keeps both its completed setup and reusable caches.
A complete repository checkpoint includes Git objects, refs, HEAD and index,
tracked working files, and nonignored untracked files. It excludes dependencies
and generated ignored caches, Git credentials/configuration, hooks, and private
runtime state. Checkpoints reject in-progress Git locks and a tree that changes
runtime state, including nested plugin repositories inside private runner caches.
Checkpoints reject in-progress Git locks and a tree that changes
during scanning. The database pointer advances only after all required objects
and the manifest have been saved. Restores verify ownership and hashes, then
publish the restored directory atomically. Git origin configuration is recreated

View File

@ -63,4 +63,18 @@ describe("sandbox work folder transport with real Node and Git", () => {
expect(paths).not.toContain(".git/config");
expect(paths.some((entry) => entry.startsWith("node_modules/"))).toBe(false);
});
it("excludes nested Git repositories in private runner state before validating their trailing slash", async () => {
const dir = await root();
await exec("git", ["init", dir]);
const privateRepo = path.join(dir, ".paperclip-runtime/session/codex-home/.tmp/plugins");
await mkdir(privateRepo, { recursive: true });
await exec("git", ["init", privateRepo]);
await writeFile(path.join(privateRepo, "private-config"), "not durable");
await writeFile(path.join(dir, "keep.txt"), "durable work");
const listed = await exec("git", ["-C", dir, "ls-files", "--others", "--exclude-standard", "-z"]);
expect(listed.stdout).toContain(".paperclip-runtime/session/codex-home/.tmp/plugins/\0");
const paths = (await transport.scan(dir, true)).map((entry) => entry.path);
expect(paths).toContain("keep.txt");
expect(paths.some((entry) => entry.includes(".paperclip-runtime"))).toBe(false);
});
});

View File

@ -113,6 +113,9 @@ function scan() {
const files = execFileSync("git", ["-C", input.root, "ls-files", "--cached", "--others", "--exclude-standard", "-z"],
{ encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).split("\0").filter(Boolean);
for (const relative of [...new Set(files)].sort()) {
// Git reports nested repositories with a trailing slash. Private runner
// caches can contain them and must be excluded before path validation.
if (relative.split("/").includes(".paperclip-runtime")) continue;
try { fs.lstatSync(full(relative)); entry(relative); } catch (error) { if (error.code !== "ENOENT") throw error; }
}
for (const name of children(gitDir)) {