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 7103949230..43262fd6ff 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 863249b7d6..60be8d2d53 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 008d909454..2724a6e28f 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 0fb575d78b..dbba40b2e1 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 8a3595b437..9e65543fa5 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 2ea08954da..06cf01d428 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 4e1bc76e93..db19d18dca 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 4091380438..dd5b5a0dea 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 817a9e7bc8..fa65c7e43d 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 93edb2a570..2571d8b8af 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 bbc9839122..6429c5f22d 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 8d21fe656a..e4035dfcda 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 2571d8b8af..8fa979d28e 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 9e65543fa5..f18dcb1895 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 c7f85b57ab..53758f6736 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 59ec9eb6eb..b860b04ab4 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 db19d18dca..762b374b94 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 9f1b35855e..896033c16a 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 dd5b5a0dea..b4e1278117 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 786b1f87a0..5d23d0c79c 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 37903a908d..258131bf2b 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 5c80fad7e9..7266752e99 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 5aa0c31e29..bcb84b4795 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 e99f932322..5e64817da6 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 7266752e99..786d3a4bed 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 bcb84b4795..983eba221c 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 e99f932322..a71fbe7b20 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 481c305d7f..f8b59bad0b 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 786d3a4bed..d338eeb818 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) {