mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 13:24:55 +01:00
feat(apps): consolidate connector management (#12684)
Completes the post-managed-OAuth connector lifecycle, Paperclip Cloud provisioning defaults, governed test flows, and consolidated Apps UI.\n\nCo-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
141f202e40
commit
1ab159d3a7
67 files changed
+1738
-1855
No files matched your search
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
ensureAgentJwtSecret,
|
||||
ensureToolActionSigningSecret,
|
||||
mergePaperclipEnvEntries,
|
||||
readAgentJwtSecretFromEnv,
|
||||
readPaperclipEnvEntries,
|
||||
@@ -24,6 +25,7 @@ describe("agent jwt env helpers", () => {
|
||||
beforeEach(() => {
|
||||
process.env = { ...ORIGINAL_ENV };
|
||||
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||
delete process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -42,6 +44,17 @@ describe("agent jwt env helpers", () => {
|
||||
expect(contents).toContain("PAPERCLIP_AGENT_JWT_SECRET=");
|
||||
});
|
||||
|
||||
it("creates an independent tool-action signing secret next to the config", () => {
|
||||
const configPath = tempConfigPath();
|
||||
const result = ensureToolActionSigningSecret(configPath);
|
||||
|
||||
expect(result.created).toBe(true);
|
||||
expect(result.secret).toHaveLength(64);
|
||||
const entries = readPaperclipEnvEntries(resolveAgentJwtEnvFile(configPath));
|
||||
expect(entries.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET).toBe(result.secret);
|
||||
expect(entries.PAPERCLIP_AGENT_JWT_SECRET).toBeUndefined();
|
||||
});
|
||||
|
||||
it("loads secret from .env next to explicit config path", () => {
|
||||
const configPath = tempConfigPath();
|
||||
const envPath = resolveAgentJwtEnvFile(configPath);
|
||||
|
||||
@@ -92,6 +92,7 @@ describe("onboard", () => {
|
||||
beforeEach(() => {
|
||||
process.env = { ...ORIGINAL_ENV };
|
||||
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||
delete process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET;
|
||||
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
||||
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
|
||||
delete process.env.PAPERCLIP_DB_BACKUP_DIR;
|
||||
@@ -248,6 +249,8 @@ describe("onboard", () => {
|
||||
expect(raw.storage.localDisk.baseDir).toBe(path.join(instanceRoot, "data", "storage"));
|
||||
expect(raw.secrets.localEncrypted.keyFilePath).toBe(path.join(instanceRoot, "secrets", "master.key"));
|
||||
expect(fs.existsSync(path.join(instanceRoot, ".env"))).toBe(true);
|
||||
expect(fs.readFileSync(path.join(instanceRoot, ".env"), "utf8"))
|
||||
.toContain("PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=");
|
||||
expect(fs.existsSync(path.join(instanceRoot, "secrets", "master.key"))).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -1471,10 +1471,12 @@ describe("worktree helpers", () => {
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const originalCwd = process.cwd();
|
||||
const originalJwtSecret = process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||
const originalToolActionSigningSecret = process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET;
|
||||
|
||||
try {
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
process.env.PAPERCLIP_AGENT_JWT_SECRET = "worktree-shared-secret";
|
||||
process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET = "worktree-tool-action-secret";
|
||||
process.chdir(repoRoot);
|
||||
|
||||
await worktreeInitCommand({
|
||||
@@ -1486,6 +1488,7 @@ describe("worktree helpers", () => {
|
||||
const envPath = path.join(repoRoot, ".paperclip", ".env");
|
||||
const envContents = fs.readFileSync(envPath, "utf8");
|
||||
expect(envContents).toContain("PAPERCLIP_AGENT_JWT_SECRET=worktree-shared-secret");
|
||||
expect(envContents).toContain("PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=worktree-tool-action-secret");
|
||||
expect(envContents).toContain("PAPERCLIP_WORKTREE_NAME=repo");
|
||||
expect(envContents).toMatch(/PAPERCLIP_WORKTREE_COLOR=\"#[0-9a-f]{6}\"/);
|
||||
} finally {
|
||||
@@ -1495,6 +1498,11 @@ describe("worktree helpers", () => {
|
||||
} else {
|
||||
process.env.PAPERCLIP_AGENT_JWT_SECRET = originalJwtSecret;
|
||||
}
|
||||
if (originalToolActionSigningSecret === undefined) {
|
||||
delete process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET;
|
||||
} else {
|
||||
process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET = originalToolActionSigningSecret;
|
||||
}
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
findPaperclipConfigKeyWarnings,
|
||||
type PaperclipConfig,
|
||||
} from "../config/schema.js";
|
||||
import { ensureAgentJwtSecret, resolveAgentJwtEnvFile } from "../config/env.js";
|
||||
import { ensureAgentJwtSecret, ensureToolActionSigningSecret, resolveAgentJwtEnvFile } from "../config/env.js";
|
||||
import { ensureLocalSecretsKeyFile } from "../config/secrets-key.js";
|
||||
import { promptDatabase } from "../prompts/database.js";
|
||||
import { promptLlm } from "../prompts/llm.js";
|
||||
@@ -453,6 +453,10 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
|
||||
} else {
|
||||
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
|
||||
}
|
||||
const toolActionSigningSecret = ensureToolActionSigningSecret(configPath);
|
||||
if (toolActionSigningSecret.created) {
|
||||
p.log.success(`Created ${pc.cyan("PAPERCLIP_TOOL_ACTION_SIGNING_SECRET")} in ${pc.dim(envFilePath)}`);
|
||||
}
|
||||
|
||||
const keyResult = ensureLocalSecretsKeyFile(existingConfig, configPath);
|
||||
if (keyResult.status === "created") {
|
||||
@@ -689,6 +693,10 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
|
||||
} else {
|
||||
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
|
||||
}
|
||||
const toolActionSigningSecret = ensureToolActionSigningSecret(configPath);
|
||||
if (toolActionSigningSecret.created) {
|
||||
p.log.success(`Created ${pc.cyan("PAPERCLIP_TOOL_ACTION_SIGNING_SECRET")} in ${pc.dim(envFilePath)}`);
|
||||
}
|
||||
|
||||
const config: PaperclipConfig = {
|
||||
$meta: {
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
prepareEmbeddedPostgresNativeRuntime,
|
||||
} from "@paperclipai/db";
|
||||
import type { Command } from "commander";
|
||||
import { ensureAgentJwtSecret, loadPaperclipEnvFile, mergePaperclipEnvEntries, readPaperclipEnvEntries, resolvePaperclipEnvFile } from "../config/env.js";
|
||||
import { ensureAgentJwtSecret, ensureToolActionSigningSecret, loadPaperclipEnvFile, mergePaperclipEnvEntries, readPaperclipEnvEntries, resolvePaperclipEnvFile } from "../config/env.js";
|
||||
import { expandHomePrefix } from "../config/home.js";
|
||||
import type { PaperclipConfig } from "../config/schema.js";
|
||||
import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
|
||||
@@ -2530,14 +2530,19 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
|
||||
const existingAgentJwtSecret =
|
||||
nonEmpty(sourceEnvEntries.PAPERCLIP_AGENT_JWT_SECRET) ??
|
||||
nonEmpty(process.env.PAPERCLIP_AGENT_JWT_SECRET);
|
||||
const existingToolActionSigningSecret =
|
||||
nonEmpty(sourceEnvEntries.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET) ??
|
||||
nonEmpty(process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET);
|
||||
mergePaperclipEnvEntries(
|
||||
{
|
||||
...buildWorktreeEnvEntries(paths, branding),
|
||||
...(existingAgentJwtSecret ? { PAPERCLIP_AGENT_JWT_SECRET: existingAgentJwtSecret } : {}),
|
||||
...(existingToolActionSigningSecret ? { PAPERCLIP_TOOL_ACTION_SIGNING_SECRET: existingToolActionSigningSecret } : {}),
|
||||
},
|
||||
paths.envPath,
|
||||
);
|
||||
ensureAgentJwtSecret(paths.configPath);
|
||||
ensureToolActionSigningSecret(paths.configPath);
|
||||
loadPaperclipEnvFile(paths.configPath);
|
||||
const copiedGitHooks = copyGitHooksToWorktreeGitDir(cwd);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { updateEnvFileContents, writeEnvFileAtomicallyIfChanged } from "@papercl
|
||||
import { resolveConfigPath } from "./store.js";
|
||||
|
||||
const JWT_SECRET_ENV_KEY = "PAPERCLIP_AGENT_JWT_SECRET";
|
||||
const TOOL_ACTION_SIGNING_SECRET_ENV_KEY = "PAPERCLIP_TOOL_ACTION_SIGNING_SECRET";
|
||||
const PAPERCLIP_OWNED_ENV_KEY_PATTERN = /^PAPERCLIP_[A-Z0-9_]+$/;
|
||||
function resolveEnvFilePath(configPath?: string) {
|
||||
return path.resolve(path.dirname(resolveConfigPath(configPath)), ".env");
|
||||
@@ -93,6 +94,25 @@ export function ensureAgentJwtSecret(configPath?: string): { secret: string; cre
|
||||
return { secret, created };
|
||||
}
|
||||
|
||||
export function ensureToolActionSigningSecret(configPath?: string): { secret: string; created: boolean } {
|
||||
loadAgentJwtEnvFile(resolveEnvFilePath(configPath));
|
||||
const existingEnv = process.env[TOOL_ACTION_SIGNING_SECRET_ENV_KEY];
|
||||
if (isNonEmpty(existingEnv)) {
|
||||
return { secret: existingEnv.trim(), created: false };
|
||||
}
|
||||
|
||||
const envFilePath = resolveEnvFilePath(configPath);
|
||||
const existingFile = readPaperclipEnvEntries(envFilePath)[TOOL_ACTION_SIGNING_SECRET_ENV_KEY];
|
||||
const secret = isNonEmpty(existingFile) ? existingFile.trim() : randomBytes(32).toString("hex");
|
||||
const created = !isNonEmpty(existingFile);
|
||||
|
||||
if (created) {
|
||||
mergePaperclipEnvEntries({ [TOOL_ACTION_SIGNING_SECRET_ENV_KEY]: secret }, envFilePath);
|
||||
}
|
||||
|
||||
return { secret, created };
|
||||
}
|
||||
|
||||
export function writeAgentJwtEnv(secret: string, filePath = resolveEnvFilePath()): void {
|
||||
mergePaperclipEnvEntries({ [JWT_SECRET_ENV_KEY]: secret }, filePath);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user