Fix worktree JWT env persistence
Ensure worktree init writes PAPERCLIP_AGENT_JWT_SECRET into the new .paperclip/.env when the source instance already has a usable secret loaded or configured. Also harden the affected integration tests against shell env leakage and full-suite timeout pressure. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -2,13 +2,14 @@ import fs from "node:fs";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
copyGitHooksToWorktreeGitDir,
|
copyGitHooksToWorktreeGitDir,
|
||||||
copySeededSecretsKey,
|
copySeededSecretsKey,
|
||||||
rebindWorkspaceCwd,
|
rebindWorkspaceCwd,
|
||||||
resolveGitWorktreeAddArgs,
|
resolveGitWorktreeAddArgs,
|
||||||
resolveWorktreeMakeTargetPath,
|
resolveWorktreeMakeTargetPath,
|
||||||
|
worktreeInitCommand,
|
||||||
worktreeMakeCommand,
|
worktreeMakeCommand,
|
||||||
} from "../commands/worktree.js";
|
} from "../commands/worktree.js";
|
||||||
import {
|
import {
|
||||||
@@ -189,7 +190,11 @@ describe("worktree helpers", () => {
|
|||||||
|
|
||||||
it("copies the source local_encrypted secrets key into the seeded worktree instance", () => {
|
it("copies the source local_encrypted secrets key into the seeded worktree instance", () => {
|
||||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
|
||||||
|
const originalInlineMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
||||||
|
const originalKeyFile = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
|
||||||
try {
|
try {
|
||||||
|
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
||||||
|
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
|
||||||
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||||
const sourceKeyPath = path.join(tempRoot, "source", "secrets", "master.key");
|
const sourceKeyPath = path.join(tempRoot, "source", "secrets", "master.key");
|
||||||
const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key");
|
const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key");
|
||||||
@@ -208,6 +213,16 @@ describe("worktree helpers", () => {
|
|||||||
|
|
||||||
expect(fs.readFileSync(targetKeyPath, "utf8")).toBe("source-master-key");
|
expect(fs.readFileSync(targetKeyPath, "utf8")).toBe("source-master-key");
|
||||||
} finally {
|
} finally {
|
||||||
|
if (originalInlineMasterKey === undefined) {
|
||||||
|
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.PAPERCLIP_SECRETS_MASTER_KEY = originalInlineMasterKey;
|
||||||
|
}
|
||||||
|
if (originalKeyFile === undefined) {
|
||||||
|
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
|
||||||
|
} else {
|
||||||
|
process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = originalKeyFile;
|
||||||
|
}
|
||||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -233,6 +248,36 @@ describe("worktree helpers", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists the current agent jwt secret into the worktree env file", async () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-jwt-"));
|
||||||
|
const repoRoot = path.join(tempRoot, "repo");
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
const originalJwtSecret = process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(repoRoot, { recursive: true });
|
||||||
|
process.env.PAPERCLIP_AGENT_JWT_SECRET = "worktree-shared-secret";
|
||||||
|
process.chdir(repoRoot);
|
||||||
|
|
||||||
|
await worktreeInitCommand({
|
||||||
|
seed: false,
|
||||||
|
fromConfig: path.join(tempRoot, "missing", "config.json"),
|
||||||
|
home: path.join(tempRoot, ".paperclip-worktrees"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const envPath = path.join(repoRoot, ".paperclip", ".env");
|
||||||
|
expect(fs.readFileSync(envPath, "utf8")).toContain("PAPERCLIP_AGENT_JWT_SECRET=worktree-shared-secret");
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
if (originalJwtSecret === undefined) {
|
||||||
|
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||||
|
} else {
|
||||||
|
process.env.PAPERCLIP_AGENT_JWT_SECRET = originalJwtSecret;
|
||||||
|
}
|
||||||
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rebinds same-repo workspace paths onto the current worktree root", () => {
|
it("rebinds same-repo workspace paths onto the current worktree root", () => {
|
||||||
expect(
|
expect(
|
||||||
rebindWorkspaceCwd({
|
rebindWorkspaceCwd({
|
||||||
@@ -315,7 +360,7 @@ describe("worktree helpers", () => {
|
|||||||
const fakeHome = path.join(tempRoot, "home");
|
const fakeHome = path.join(tempRoot, "home");
|
||||||
const worktreePath = path.join(fakeHome, "paperclip-make-test");
|
const worktreePath = path.join(fakeHome, "paperclip-make-test");
|
||||||
const originalCwd = process.cwd();
|
const originalCwd = process.cwd();
|
||||||
const originalHome = process.env.HOME;
|
const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue(fakeHome);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(repoRoot, { recursive: true });
|
fs.mkdirSync(repoRoot, { recursive: true });
|
||||||
@@ -327,7 +372,6 @@ describe("worktree helpers", () => {
|
|||||||
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
|
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
|
||||||
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
|
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
|
||||||
|
|
||||||
process.env.HOME = fakeHome;
|
|
||||||
process.chdir(repoRoot);
|
process.chdir(repoRoot);
|
||||||
|
|
||||||
await worktreeMakeCommand("paperclip-make-test", {
|
await worktreeMakeCommand("paperclip-make-test", {
|
||||||
@@ -340,12 +384,8 @@ describe("worktree helpers", () => {
|
|||||||
expect(fs.existsSync(path.join(worktreePath, ".paperclip", ".env"))).toBe(true);
|
expect(fs.existsSync(path.join(worktreePath, ".paperclip", ".env"))).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
process.chdir(originalCwd);
|
process.chdir(originalCwd);
|
||||||
if (originalHome === undefined) {
|
homedirSpy.mockRestore();
|
||||||
delete process.env.HOME;
|
|
||||||
} else {
|
|
||||||
process.env.HOME = originalHome;
|
|
||||||
}
|
|
||||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
}, 20_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -642,7 +642,17 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
writeConfig(targetConfig, paths.configPath);
|
writeConfig(targetConfig, paths.configPath);
|
||||||
mergePaperclipEnvEntries(buildWorktreeEnvEntries(paths), paths.envPath);
|
const sourceEnvEntries = readPaperclipEnvEntries(resolvePaperclipEnvFile(sourceConfigPath));
|
||||||
|
const existingAgentJwtSecret =
|
||||||
|
nonEmpty(sourceEnvEntries.PAPERCLIP_AGENT_JWT_SECRET) ??
|
||||||
|
nonEmpty(process.env.PAPERCLIP_AGENT_JWT_SECRET);
|
||||||
|
mergePaperclipEnvEntries(
|
||||||
|
{
|
||||||
|
...buildWorktreeEnvEntries(paths),
|
||||||
|
...(existingAgentJwtSecret ? { PAPERCLIP_AGENT_JWT_SECRET: existingAgentJwtSecret } : {}),
|
||||||
|
},
|
||||||
|
paths.envPath,
|
||||||
|
);
|
||||||
ensureAgentJwtSecret(paths.configPath);
|
ensureAgentJwtSecret(paths.configPath);
|
||||||
loadPaperclipEnvFile(paths.configPath);
|
loadPaperclipEnvFile(paths.configPath);
|
||||||
const copiedGitHooks = copyGitHooksToWorktreeGitDir(cwd);
|
const copiedGitHooks = copyGitHooksToWorktreeGitDir(cwd);
|
||||||
|
|||||||
@@ -52,5 +52,5 @@ describe("privateHostnameGuard", () => {
|
|||||||
const res = await request(app).get("/dashboard").set("Host", "dotta-macbook-pro:3100");
|
const res = await request(app).get("/dashboard").set("Host", "dotta-macbook-pro:3100");
|
||||||
expect(res.status).toBe(403);
|
expect(res.status).toBe(403);
|
||||||
expect(res.text).toContain("please run pnpm paperclipai allowed-hostname dotta-macbook-pro");
|
expect(res.text).toContain("please run pnpm paperclipai allowed-hostname dotta-macbook-pro");
|
||||||
});
|
}, 20_000);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user