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:
Dotta authored and GitHub committed 2026-09-01 14:55:35 -05:00
1 parent 141f202e40
commit 1ab159d3a7
67 files changed
+1738 -1855

No files matched your search

+13
View File
@@ -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);
+3
View File
@@ -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);
});
+8
View File
@@ -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 });
}
});
+9 -1
View File
@@ -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: {
+6 -1
View File
@@ -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);
+20
View File
@@ -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);
}