From 346152f67d784893295c9b6fc0e08f0adfbc0b1c Mon Sep 17 00:00:00 2001 From: Fahmin Date: Sun, 8 Mar 2026 00:15:56 +0530 Subject: [PATCH 01/18] fix(scripts): use shell on Windows to fix spawn EINVAL in dev-runner --- scripts/dev-runner.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs index 71039492..43262fd6 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -47,7 +47,7 @@ const serverScript = mode === "watch" ? "dev:watch" : "dev"; const child = spawn( pnpmBin, ["--filter", "@paperclipai/server", serverScript, ...forwardedArgs], - { stdio: "inherit", env }, + { stdio: "inherit", env, shell: process.platform === "win32" }, ); child.on("exit", (code, signal) => { From 609b55f5300975807dc1e862f330d1a22dd04ad3 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 16:04:00 -0800 Subject: [PATCH 02/18] fix(cli): split path and query in buildUrl to prevent %3F encoding The URL constructor's pathname setter encodes ? as %3F, breaking heartbeat event polling. Split query params before assignment. Fixes #204 Co-Authored-By: Claude Opus 4.6 --- cli/src/client/http.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/src/client/http.ts b/cli/src/client/http.ts index 863249b7..60be8d2d 100644 --- a/cli/src/client/http.ts +++ b/cli/src/client/http.ts @@ -104,8 +104,10 @@ export class PaperclipApiClient { function buildUrl(apiBase: string, path: string): string { const normalizedPath = path.startsWith("/") ? path : `/${path}`; + const [pathname, query] = normalizedPath.split("?"); const url = new URL(apiBase); - url.pathname = `${url.pathname.replace(/\/+$/, "")}${normalizedPath}`; + url.pathname = `${url.pathname.replace(/\/+$/, "")}${pathname}`; + if (query) url.search = query; return url.toString(); } From 977f5570be148fc7741f3aa03f54cc5cfa4e4145 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 16:04:09 -0800 Subject: [PATCH 03/18] fix(server): redact secret-sourced env vars in run logs by provenance resolveAdapterConfigForRuntime now returns a secretKeys set tracking which env vars came from secret_ref bindings. The onAdapterMeta callback uses this to redact them regardless of key name. Fixes #234 Co-Authored-By: Claude Opus 4.6 --- server/src/routes/agents.ts | 6 +++--- server/src/services/heartbeat.ts | 7 ++++++- server/src/services/secrets.ts | 10 ++++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 008d9094..2724a6e2 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -211,7 +211,7 @@ export function agentRoutes(db: Db) { adapterConfig: Record, ) { if (adapterType !== "opencode_local") return; - const runtimeConfig = await secretsSvc.resolveAdapterConfigForRuntime(companyId, adapterConfig); + const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(companyId, adapterConfig); const runtimeEnv = asRecord(runtimeConfig.env) ?? {}; try { await ensureOpenCodeModelConfiguredAndAvailable({ @@ -386,7 +386,7 @@ export function agentRoutes(db: Db) { inputAdapterConfig, { strictMode: strictSecretsMode }, ); - const runtimeAdapterConfig = await secretsSvc.resolveAdapterConfigForRuntime( + const { config: runtimeAdapterConfig } = await secretsSvc.resolveAdapterConfigForRuntime( companyId, normalizedAdapterConfig, ); @@ -1226,7 +1226,7 @@ export function agentRoutes(db: Db) { } const config = asRecord(agent.adapterConfig) ?? {}; - const runtimeConfig = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config); + const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config); const result = await runClaudeLogin({ runId: `claude-login-${randomUUID()}`, agent: { diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 0fb575d7..dbba40b2 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -1240,11 +1240,16 @@ export function heartbeatService(db: Db) { const mergedConfig = issueAssigneeOverrides?.adapterConfig ? { ...config, ...issueAssigneeOverrides.adapterConfig } : config; - const resolvedConfig = await secretsSvc.resolveAdapterConfigForRuntime( + const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime( agent.companyId, mergedConfig, ); const onAdapterMeta = async (meta: AdapterInvocationMeta) => { + if (meta.env && secretKeys.size > 0) { + for (const key of secretKeys) { + if (key in meta.env) meta.env[key] = "***REDACTED***"; + } + } await appendRunEvent(currentRun, seq++, { eventType: "adapter.invoke", stream: "system", diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 8a3595b4..9e65543f 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -331,15 +331,16 @@ export function secretService(db: Db) { return resolved; }, - resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record) => { + resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record): Promise<{ config: Record; secretKeys: Set }> => { const resolved = { ...adapterConfig }; + const secretKeys = new Set(); if (!Object.prototype.hasOwnProperty.call(adapterConfig, "env")) { - return resolved; + return { config: resolved, secretKeys }; } const record = asRecord(adapterConfig.env); if (!record) { resolved.env = {}; - return resolved; + return { config: resolved, secretKeys }; } const env: Record = {}; for (const [key, rawBinding] of Object.entries(record)) { @@ -355,10 +356,11 @@ export function secretService(db: Db) { env[key] = binding.value; } else { env[key] = await resolveSecretValue(companyId, binding.secretId, binding.version); + secretKeys.add(key); } } resolved.env = env; - return resolved; + return { config: resolved, secretKeys }; }, }; } From 53c944e8bcc49b535833754aefee40b03fb77bc7 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 16:04:17 -0800 Subject: [PATCH 04/18] fix(ui): prevent blank screen when prompt template is emptied Change onChange handler from v || undefined to v ?? "" so empty strings don't become undefined and crash downstream .trim() calls. Fixes #191 Co-Authored-By: Claude Opus 4.6 --- ui/src/components/AgentConfigForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 2ea08954..06cf01d4 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -441,7 +441,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { "promptTemplate", String(config.promptTemplate ?? ""), )} - onChange={(v) => mark("adapterConfig", "promptTemplate", v || undefined)} + onChange={(v) => mark("adapterConfig", "promptTemplate", v ?? "")} placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..." contentClassName="min-h-[88px] text-sm font-mono" imageUploadHandler={async (file) => { From b886eb3cf0fe0b6ae3e21d9e902c5344249289e1 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 16:04:22 -0800 Subject: [PATCH 05/18] feat(ui): show human-readable role labels in agent list and properties Use roleLabels lookup in list view subtitle and AgentProperties panel instead of raw role strings. Fixes #180 Co-Authored-By: Claude Opus 4.6 --- ui/src/components/AgentProperties.tsx | 8 +++++++- ui/src/pages/Agents.tsx | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/src/components/AgentProperties.tsx b/ui/src/components/AgentProperties.tsx index 4e1bc76e..db19d18d 100644 --- a/ui/src/components/AgentProperties.tsx +++ b/ui/src/components/AgentProperties.tsx @@ -25,6 +25,12 @@ const adapterLabels: Record = { http: "HTTP", }; +const roleLabels: Record = { + ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO", + engineer: "Engineer", designer: "Designer", pm: "PM", + qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General", +}; + function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) { return (
@@ -52,7 +58,7 @@ export function AgentProperties({ agent, runtimeState }: AgentPropertiesProps) { - {agent.role} + {roleLabels[agent.role] ?? agent.role} {agent.title && ( diff --git a/ui/src/pages/Agents.tsx b/ui/src/pages/Agents.tsx index 40913804..dd5b5a0d 100644 --- a/ui/src/pages/Agents.tsx +++ b/ui/src/pages/Agents.tsx @@ -231,7 +231,7 @@ export function Agents() { From 99330390943052e533de2d33f0f865af8bec8c3e Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 16:04:28 -0800 Subject: [PATCH 06/18] fix(server): auto-deduplicate agent names on creation instead of rejecting Replace assertCompanyShortnameAvailable with deduplicateAgentName in the create path so duplicate names get auto-suffixed (e.g. Engineer 2) instead of throwing a conflict error. Fixes #232 Co-Authored-By: Claude Opus 4.6 --- server/src/services/agents.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/services/agents.ts b/server/src/services/agents.ts index 817a9e7b..fa65c7e4 100644 --- a/server/src/services/agents.ts +++ b/server/src/services/agents.ts @@ -341,13 +341,17 @@ export function agentService(db: Db) { await ensureManager(companyId, data.reportsTo); } - await assertCompanyShortnameAvailable(companyId, data.name); + const existingAgents = await db + .select({ id: agents.id, name: agents.name, status: agents.status }) + .from(agents) + .where(eq(agents.companyId, companyId)); + const uniqueName = deduplicateAgentName(data.name, existingAgents); const role = data.role ?? "general"; const normalizedPermissions = normalizeAgentPermissions(data.permissions, role); const created = await db .insert(agents) - .values({ ...data, companyId, role, permissions: normalizedPermissions }) + .values({ ...data, name: uniqueName, companyId, role, permissions: normalizedPermissions }) .returning() .then((rows) => rows[0]); From c59e059976c2556927eb10bf93487105e2489e79 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 16:17:45 -0800 Subject: [PATCH 07/18] fix(db): use fileURLToPath for Windows-safe migration paths URL.pathname returns /C:/... on Windows, causing doubled drive letters when Node prepends the current drive. fileURLToPath handles this correctly across platforms. Fixes #132 Co-Authored-By: Claude Opus 4.6 --- packages/db/src/client.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index 93edb2a5..2571d8b8 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -2,12 +2,13 @@ import { createHash } from "node:crypto"; import { drizzle as drizzlePg } from "drizzle-orm/postgres-js"; import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator"; import { readFile, readdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; import postgres from "postgres"; import * as schema from "./schema/index.js"; -const MIGRATIONS_FOLDER = new URL("./migrations", import.meta.url).pathname; +const MIGRATIONS_FOLDER = fileURLToPath(new URL("./migrations", import.meta.url)); const DRIZZLE_MIGRATIONS_TABLE = "__drizzle_migrations"; -const MIGRATIONS_JOURNAL_JSON = new URL("./migrations/meta/_journal.json", import.meta.url).pathname; +const MIGRATIONS_JOURNAL_JSON = fileURLToPath(new URL("./migrations/meta/_journal.json", import.meta.url)); function isSafeIdentifier(value: string): boolean { return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value); @@ -702,7 +703,7 @@ export async function migratePostgresIfEmpty(url: string): Promise Date: Sat, 7 Mar 2026 16:18:14 -0800 Subject: [PATCH 08/18] fix(server): use home-based path for run logs instead of cwd Run logs defaulted to process.cwd()/data/run-logs, placing logs in unexpected locations when launched from non-home directories. Now defaults to ~/.paperclip/instances//data/run-logs/. Fixes #89 Co-Authored-By: Claude Opus 4.6 --- server/src/services/run-log-store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/services/run-log-store.ts b/server/src/services/run-log-store.ts index bbc98391..6429c5f2 100644 --- a/server/src/services/run-log-store.ts +++ b/server/src/services/run-log-store.ts @@ -2,6 +2,7 @@ import { createReadStream, promises as fs } from "node:fs"; import path from "node:path"; import { createHash } from "node:crypto"; import { notFound } from "../errors.js"; +import { resolvePaperclipInstanceRoot } from "../home-paths.js"; export type RunLogStoreType = "local_file"; @@ -148,7 +149,7 @@ let cachedStore: RunLogStore | null = null; export function getRunLogStore() { if (cachedStore) return cachedStore; - const basePath = process.env.RUN_LOG_BASE_PATH ?? path.resolve(process.cwd(), "data/run-logs"); + const basePath = process.env.RUN_LOG_BASE_PATH ?? path.resolve(resolvePaperclipInstanceRoot(), "data", "run-logs"); cachedStore = createLocalFileRunLogStore(basePath); return cachedStore; } From 20b171bd168915b678658965942aa982d21f1006 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 16:18:41 -0800 Subject: [PATCH 09/18] fix(server): wake agent when issue status changes from backlog Previously, agents were only woken when the assignee changed. Now also wakes the assigned agent when an issue transitions out of backlog status (e.g. backlog -> todo). Fixes #167 Co-Authored-By: Claude Opus 4.6 --- server/src/routes/issues.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 8d21fe65..e4035dfc 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -575,6 +575,10 @@ export function issueRoutes(db: Db, storage: StorageService) { } const assigneeChanged = assigneeWillChange; + const statusChangedFromBacklog = + existing.status === "backlog" && + issue.status !== "backlog" && + req.body.status !== undefined; // Merge all wakeups from this update into one enqueue per agent to avoid duplicate runs. void (async () => { @@ -592,6 +596,18 @@ export function issueRoutes(db: Db, storage: StorageService) { }); } + if (!assigneeChanged && statusChangedFromBacklog && issue.assigneeAgentId) { + wakeups.set(issue.assigneeAgentId, { + source: "automation", + triggerDetail: "system", + reason: "issue_status_changed", + payload: { issueId: issue.id, mutation: "update" }, + requestedByActorType: actor.actorType, + requestedByActorId: actor.actorId, + contextSnapshot: { issueId: issue.id, source: "issue.status_change" }, + }); + } + if (commentBody && comment) { let mentionedIds: string[] = []; try { From 54b512f9e0c5a01fa718a72239e6ee057aedbbb9 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 17:01:36 -0800 Subject: [PATCH 10/18] fix(db): reuse MIGRATIONS_FOLDER constant instead of recomputing The local migrationsFolder variable in migratePostgresIfEmpty duplicated the module-level MIGRATIONS_FOLDER constant. Reuse the constant to keep a single source of truth for the migration path. Co-Authored-By: Claude Opus 4.6 --- packages/db/src/client.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index 2571d8b8..8fa979d2 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -703,8 +703,7 @@ export async function migratePostgresIfEmpty(url: string): Promise Date: Sat, 7 Mar 2026 17:05:55 -0800 Subject: [PATCH 11/18] fix(secrets): add secretKeys tracking to resolveEnvBindings for consistent redaction resolveEnvBindings now returns { env, secretKeys } matching the pattern already used by resolveAdapterConfigForRuntime, so any caller can redact secret-sourced values by provenance rather than key-name heuristics alone. Co-Authored-By: Claude Opus 4.6 --- server/src/services/secrets.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 9e65543f..f18dcb18 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -308,10 +308,11 @@ export function secretService(db: Db) { return normalized; }, - resolveEnvBindings: async (companyId: string, envValue: unknown) => { + resolveEnvBindings: async (companyId: string, envValue: unknown): Promise<{ env: Record; secretKeys: Set }> => { const record = asRecord(envValue); - if (!record) return {} as Record; + if (!record) return { env: {} as Record, secretKeys: new Set() }; const resolved: Record = {}; + const secretKeys = new Set(); for (const [key, rawBinding] of Object.entries(record)) { if (!ENV_KEY_RE.test(key)) { @@ -326,9 +327,10 @@ export function secretService(db: Db) { resolved[key] = binding.value; } else { resolved[key] = await resolveSecretValue(companyId, binding.secretId, binding.version); + secretKeys.add(key); } } - return resolved; + return { env: resolved, secretKeys }; }, resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record): Promise<{ config: Record; secretKeys: Set }> => { From 2639184f46179427ff361d4ef93e021cc6e7860a Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 17:07:14 -0800 Subject: [PATCH 12/18] refactor: extract roleLabels to shared constants Move duplicated roleLabels map from AgentProperties.tsx, Agents.tsx, OrgChart.tsx, and agent-config-primitives.tsx into AGENT_ROLE_LABELS in packages/shared/src/constants.ts. Co-Authored-By: Claude Opus 4.6 --- packages/shared/src/constants.ts | 14 ++++++++++++++ packages/shared/src/index.ts | 1 + ui/src/components/AgentProperties.tsx | 8 ++------ ui/src/components/agent-config-primitives.tsx | 7 ++----- ui/src/pages/Agents.tsx | 8 ++------ ui/src/pages/OrgChart.tsx | 8 ++------ 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index c7f85b57..53758f67 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -49,6 +49,20 @@ export const AGENT_ROLES = [ ] as const; export type AgentRole = (typeof AGENT_ROLES)[number]; +export const AGENT_ROLE_LABELS: Record = { + ceo: "CEO", + cto: "CTO", + cmo: "CMO", + cfo: "CFO", + engineer: "Engineer", + designer: "Designer", + pm: "PM", + qa: "QA", + devops: "DevOps", + researcher: "Researcher", + general: "General", +}; + export const AGENT_ICON_NAMES = [ "bot", "cpu", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 59ec9eb6..b860b04a 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -6,6 +6,7 @@ export { AGENT_STATUSES, AGENT_ADAPTER_TYPES, AGENT_ROLES, + AGENT_ROLE_LABELS, AGENT_ICON_NAMES, ISSUE_STATUSES, ISSUE_PRIORITIES, diff --git a/ui/src/components/AgentProperties.tsx b/ui/src/components/AgentProperties.tsx index db19d18d..762b374b 100644 --- a/ui/src/components/AgentProperties.tsx +++ b/ui/src/components/AgentProperties.tsx @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { Link } from "@/lib/router"; -import type { Agent, AgentRuntimeState } from "@paperclipai/shared"; +import { AGENT_ROLE_LABELS, type Agent, type AgentRuntimeState } from "@paperclipai/shared"; import { agentsApi } from "../api/agents"; import { useCompany } from "../context/CompanyContext"; import { queryKeys } from "../lib/queryKeys"; @@ -25,11 +25,7 @@ const adapterLabels: Record = { http: "HTTP", }; -const roleLabels: Record = { - ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO", - engineer: "Engineer", designer: "Designer", pm: "PM", - qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General", -}; +const roleLabels = AGENT_ROLE_LABELS as Record; function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) { return ( diff --git a/ui/src/components/agent-config-primitives.tsx b/ui/src/components/agent-config-primitives.tsx index 9f1b3585..896033c1 100644 --- a/ui/src/components/agent-config-primitives.tsx +++ b/ui/src/components/agent-config-primitives.tsx @@ -15,6 +15,7 @@ import { import { Button } from "@/components/ui/button"; import { HelpCircle, ChevronDown, ChevronRight } from "lucide-react"; import { cn } from "../lib/utils"; +import { AGENT_ROLE_LABELS } from "@paperclipai/shared"; /* ---- Help text for (?) tooltips ---- */ export const help: Record = { @@ -60,11 +61,7 @@ export const adapterLabels: Record = { http: "HTTP", }; -export const roleLabels: Record = { - ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO", - engineer: "Engineer", designer: "Designer", pm: "PM", - qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General", -}; +export const roleLabels = AGENT_ROLE_LABELS as Record; /* ---- Primitive components ---- */ diff --git a/ui/src/pages/Agents.tsx b/ui/src/pages/Agents.tsx index dd5b5a0d..b4e12781 100644 --- a/ui/src/pages/Agents.tsx +++ b/ui/src/pages/Agents.tsx @@ -18,7 +18,7 @@ import { PageTabBar } from "../components/PageTabBar"; import { Tabs } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Bot, Plus, List, GitBranch, SlidersHorizontal } from "lucide-react"; -import type { Agent } from "@paperclipai/shared"; +import { AGENT_ROLE_LABELS, type Agent } from "@paperclipai/shared"; const adapterLabels: Record = { claude_local: "Claude", @@ -31,11 +31,7 @@ const adapterLabels: Record = { http: "HTTP", }; -const roleLabels: Record = { - ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO", - engineer: "Engineer", designer: "Designer", pm: "PM", - qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General", -}; +const roleLabels = AGENT_ROLE_LABELS as Record; type FilterTab = "all" | "active" | "paused" | "error"; diff --git a/ui/src/pages/OrgChart.tsx b/ui/src/pages/OrgChart.tsx index 786b1f87..5d23d0c7 100644 --- a/ui/src/pages/OrgChart.tsx +++ b/ui/src/pages/OrgChart.tsx @@ -10,7 +10,7 @@ import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; import { AgentIcon } from "../components/AgentIconPicker"; import { Network } from "lucide-react"; -import type { Agent } from "@paperclipai/shared"; +import { AGENT_ROLE_LABELS, type Agent } from "@paperclipai/shared"; // Layout constants const CARD_W = 200; @@ -422,11 +422,7 @@ export function OrgChart() { ); } -const roleLabels: Record = { - ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO", - engineer: "Engineer", designer: "Designer", pm: "PM", - qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General", -}; +const roleLabels = AGENT_ROLE_LABELS as Record; function roleLabel(role: string): string { return roleLabels[role] ?? role; From 3860812323ee7de7fc2624e175acbe8f650d6765 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 8 Mar 2026 09:38:32 +0800 Subject: [PATCH 13/18] feat: add auth.disableSignUp config option - Added disableSignUp to authConfigSchema in config-schema.ts - Added authDisableSignUp to Config interface - Added parsing from PAPERCLIP_AUTH_DISABLE_SIGN_UP env or config file - Passed to better-auth emailAndPassword.disableSignUp When true, blocks new user registrations on public instances. Defaults to false (backward compatible). Fixes #241 --- packages/shared/src/config-schema.ts | 2 ++ server/src/auth/better-auth.ts | 1 + server/src/config.ts | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/packages/shared/src/config-schema.ts b/packages/shared/src/config-schema.ts index 37903a90..258131bf 100644 --- a/packages/shared/src/config-schema.ts +++ b/packages/shared/src/config-schema.ts @@ -55,6 +55,7 @@ export const serverConfigSchema = z.object({ export const authConfigSchema = z.object({ baseUrlMode: z.enum(AUTH_BASE_URL_MODES).default("auto"), publicBaseUrl: z.string().url().optional(), + disableSignUp: z.boolean().default(false), }); export const storageLocalDiskConfigSchema = z.object({ @@ -103,6 +104,7 @@ export const paperclipConfigSchema = z server: serverConfigSchema, auth: authConfigSchema.default({ baseUrlMode: "auto", + disableSignUp: false, }), storage: storageConfigSchema.default({ provider: "local_disk", diff --git a/server/src/auth/better-auth.ts b/server/src/auth/better-auth.ts index 5c80fad7..7266752e 100644 --- a/server/src/auth/better-auth.ts +++ b/server/src/auth/better-auth.ts @@ -86,6 +86,7 @@ export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins? emailAndPassword: { enabled: true, requireEmailVerification: false, + disableSignUp: config.authDisableSignUp ?? false, }, }; diff --git a/server/src/config.ts b/server/src/config.ts index 5aa0c31e..bcb84b47 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -37,6 +37,7 @@ export interface Config { allowedHostnames: string[]; authBaseUrlMode: AuthBaseUrlMode; authPublicBaseUrl: string | undefined; + authDisableSignUp: boolean; databaseMode: DatabaseMode; databaseUrl: string | undefined; embeddedPostgresDataDir: string; @@ -142,6 +143,9 @@ export function loadConfig(): Config { authBaseUrlModeFromEnv ?? fileConfig?.auth?.baseUrlMode ?? (authPublicBaseUrl ? "explicit" : "auto"); + const authDisableSignUp: boolean = + process.env.PAPERCLIP_AUTH_DISABLE_SIGN_UP === "true" || + fileConfig?.auth?.disableSignUp === true; const allowedHostnamesFromEnvRaw = process.env.PAPERCLIP_ALLOWED_HOSTNAMES; const allowedHostnamesFromEnv = allowedHostnamesFromEnvRaw ? allowedHostnamesFromEnvRaw @@ -203,6 +207,7 @@ export function loadConfig(): Config { allowedHostnames, authBaseUrlMode, authPublicBaseUrl, + authDisableSignUp, databaseMode: fileDatabaseMode, databaseUrl: process.env.DATABASE_URL ?? fileDbUrl, embeddedPostgresDataDir: resolveHomeAwarePath( From 77e06c57f9456bc0ae966c0b8358b1d9e5f420bb Mon Sep 17 00:00:00 2001 From: Ming Fang Date: Sat, 7 Mar 2026 21:15:12 -0500 Subject: [PATCH 14/18] Add pi-local package.json to Dockerfile --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index e99f9323..5e64817d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,8 @@ COPY packages/adapters/codex-local/package.json packages/adapters/codex-local/ COPY packages/adapters/cursor-local/package.json packages/adapters/cursor-local/ COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/ COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/ +COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/ + RUN pnpm install --frozen-lockfile FROM base AS build From 91fda5d04f95aca1c97fcb0ed3696fbcb9022fc1 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 8 Mar 2026 10:18:27 +0800 Subject: [PATCH 15/18] fix: correct env var priority for authDisableSignUp - Env var now properly overrides file config in both directions - Follows established pattern for boolean config flags - Removed redundant ?? false (field is typed boolean) - PAPERCLIP_AUTH_DISABLE_SIGN_UP can now set to 'false' to override file config's 'true' --- server/src/auth/better-auth.ts | 2 +- server/src/config.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/auth/better-auth.ts b/server/src/auth/better-auth.ts index 7266752e..786d3a4b 100644 --- a/server/src/auth/better-auth.ts +++ b/server/src/auth/better-auth.ts @@ -86,7 +86,7 @@ export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins? emailAndPassword: { enabled: true, requireEmailVerification: false, - disableSignUp: config.authDisableSignUp ?? false, + disableSignUp: config.authDisableSignUp, }, }; diff --git a/server/src/config.ts b/server/src/config.ts index bcb84b47..983eba22 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -143,9 +143,11 @@ export function loadConfig(): Config { authBaseUrlModeFromEnv ?? fileConfig?.auth?.baseUrlMode ?? (authPublicBaseUrl ? "explicit" : "auto"); + const disableSignUpFromEnv = process.env.PAPERCLIP_AUTH_DISABLE_SIGN_UP; const authDisableSignUp: boolean = - process.env.PAPERCLIP_AUTH_DISABLE_SIGN_UP === "true" || - fileConfig?.auth?.disableSignUp === true; + disableSignUpFromEnv !== undefined + ? disableSignUpFromEnv === "true" + : (fileConfig?.auth?.disableSignUp ?? false); const allowedHostnamesFromEnvRaw = process.env.PAPERCLIP_ALLOWED_HOSTNAMES; const allowedHostnamesFromEnv = allowedHostnamesFromEnvRaw ? allowedHostnamesFromEnvRaw From ff3f04ff4803695768e20e21edde8c03da345f28 Mon Sep 17 00:00:00 2001 From: Ming Fang Date: Sat, 7 Mar 2026 21:24:56 -0500 Subject: [PATCH 16/18] Add opencode-ai to global npm install in Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e99f9323..a71fbe7b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ RUN test -f server/dist/index.js || (echo "ERROR: server build output missing" & FROM base AS production WORKDIR /app COPY --from=build /app /app -RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest +RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest opencode-ai ENV NODE_ENV=production \ HOME=/paperclip \ From 4e01633202cc53a6a0b92a366482c43526d19e53 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Sat, 7 Mar 2026 21:57:06 -0500 Subject: [PATCH 17/18] feat(adapters): add claude-sonnet-4-6 and claude-haiku-4-6 models --- packages/adapters/claude-local/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/adapters/claude-local/src/index.ts b/packages/adapters/claude-local/src/index.ts index 481c305d..f8b59bad 100644 --- a/packages/adapters/claude-local/src/index.ts +++ b/packages/adapters/claude-local/src/index.ts @@ -3,6 +3,8 @@ export const label = "Claude Code (local)"; export const models = [ { id: "claude-opus-4-6", label: "Claude Opus 4.6" }, + { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" }, + { id: "claude-haiku-4-6", label: "Claude Haiku 4.6" }, { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" }, { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" }, ]; From ad55af04ccc4801221a4c94cd006884f4b52b31a Mon Sep 17 00:00:00 2001 From: Dale Stubblefield Date: Sun, 8 Mar 2026 22:00:51 -0500 Subject: [PATCH 18/18] fix: disable secure cookies for HTTP deployments Fixes login failing silently on authenticated + private deployments served over plain HTTP (e.g. Tailscale, LAN). Users can sign up and sign in, but the session cookie is rejected by the browser so they are immediately redirected back to the login page. Better Auth defaults to __Secure- prefixed cookies with the Secure flag when NODE_ENV=production. Browsers silently reject Secure cookies on non-HTTPS origins. This detects when PAPERCLIP_PUBLIC_URL uses http:// and sets useSecureCookies: false so session cookies work without HTTPS. --- server/src/auth/better-auth.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/auth/better-auth.ts b/server/src/auth/better-auth.ts index 786d3a4b..d338eeb8 100644 --- a/server/src/auth/better-auth.ts +++ b/server/src/auth/better-auth.ts @@ -70,6 +70,9 @@ export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins? const secret = process.env.BETTER_AUTH_SECRET ?? process.env.PAPERCLIP_AGENT_JWT_SECRET ?? "paperclip-dev-secret"; const effectiveTrustedOrigins = trustedOrigins ?? deriveAuthTrustedOrigins(config); + const publicUrl = process.env.PAPERCLIP_PUBLIC_URL ?? baseUrl; + const isHttpOnly = publicUrl ? publicUrl.startsWith("http://") : false; + const authConfig = { baseURL: baseUrl, secret, @@ -88,6 +91,7 @@ export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins? requireEmailVerification: false, disableSignUp: config.authDisableSignUp, }, + ...(isHttpOnly ? { advanced: { useSecureCookies: false } } : {}), }; if (!baseUrl) {