Merge remote-tracking branch 'public-gh/master'
* public-gh/master: fix: disable secure cookies for HTTP deployments feat(adapters): add claude-sonnet-4-6 and claude-haiku-4-6 models Add opencode-ai to global npm install in Dockerfile fix: correct env var priority for authDisableSignUp Add pi-local package.json to Dockerfile feat: add auth.disableSignUp config option refactor: extract roleLabels to shared constants fix(secrets): add secretKeys tracking to resolveEnvBindings for consistent redaction fix(db): reuse MIGRATIONS_FOLDER constant instead of recomputing fix(server): wake agent when issue status changes from backlog fix(server): use home-based path for run logs instead of cwd fix(db): use fileURLToPath for Windows-safe migration paths fix(server): auto-deduplicate agent names on creation instead of rejecting feat(ui): show human-readable role labels in agent list and properties fix(ui): prevent blank screen when prompt template is emptied fix(server): redact secret-sourced env vars in run logs by provenance fix(cli): split path and query in buildUrl to prevent %3F encoding fix(scripts): use shell on Windows to fix spawn EINVAL in dev-runner
This commit is contained in:
@@ -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/cursor-local/package.json packages/adapters/cursor-local/
|
||||||
COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/
|
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/opencode-local/package.json packages/adapters/opencode-local/
|
||||||
|
COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
@@ -31,7 +33,7 @@ RUN test -f server/dist/index.js || (echo "ERROR: server build output missing" &
|
|||||||
FROM base AS production
|
FROM base AS production
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app /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 \
|
ENV NODE_ENV=production \
|
||||||
HOME=/paperclip \
|
HOME=/paperclip \
|
||||||
|
|||||||
@@ -104,8 +104,10 @@ export class PaperclipApiClient {
|
|||||||
|
|
||||||
function buildUrl(apiBase: string, path: string): string {
|
function buildUrl(apiBase: string, path: string): string {
|
||||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||||
|
const [pathname, query] = normalizedPath.split("?");
|
||||||
const url = new URL(apiBase);
|
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();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export const label = "Claude Code (local)";
|
|||||||
|
|
||||||
export const models = [
|
export const models = [
|
||||||
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
|
{ 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-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" },
|
||||||
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
|
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { createHash } from "node:crypto";
|
|||||||
import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
|
import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
|
||||||
import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator";
|
import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator";
|
||||||
import { readFile, readdir } from "node:fs/promises";
|
import { readFile, readdir } from "node:fs/promises";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
import * as schema from "./schema/index.js";
|
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 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 {
|
function isSafeIdentifier(value: string): boolean {
|
||||||
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
||||||
@@ -702,8 +703,7 @@ export async function migratePostgresIfEmpty(url: string): Promise<MigrationBoot
|
|||||||
}
|
}
|
||||||
|
|
||||||
const db = drizzlePg(sql);
|
const db = drizzlePg(sql);
|
||||||
const migrationsFolder = new URL("./migrations", import.meta.url).pathname;
|
await migratePg(db, { migrationsFolder: MIGRATIONS_FOLDER });
|
||||||
await migratePg(db, { migrationsFolder });
|
|
||||||
|
|
||||||
return { migrated: true, reason: "migrated-empty-db", tableCount: 0 };
|
return { migrated: true, reason: "migrated-empty-db", tableCount: 0 };
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export const serverConfigSchema = z.object({
|
|||||||
export const authConfigSchema = z.object({
|
export const authConfigSchema = z.object({
|
||||||
baseUrlMode: z.enum(AUTH_BASE_URL_MODES).default("auto"),
|
baseUrlMode: z.enum(AUTH_BASE_URL_MODES).default("auto"),
|
||||||
publicBaseUrl: z.string().url().optional(),
|
publicBaseUrl: z.string().url().optional(),
|
||||||
|
disableSignUp: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const storageLocalDiskConfigSchema = z.object({
|
export const storageLocalDiskConfigSchema = z.object({
|
||||||
@@ -103,6 +104,7 @@ export const paperclipConfigSchema = z
|
|||||||
server: serverConfigSchema,
|
server: serverConfigSchema,
|
||||||
auth: authConfigSchema.default({
|
auth: authConfigSchema.default({
|
||||||
baseUrlMode: "auto",
|
baseUrlMode: "auto",
|
||||||
|
disableSignUp: false,
|
||||||
}),
|
}),
|
||||||
storage: storageConfigSchema.default({
|
storage: storageConfigSchema.default({
|
||||||
provider: "local_disk",
|
provider: "local_disk",
|
||||||
|
|||||||
@@ -48,6 +48,20 @@ export const AGENT_ROLES = [
|
|||||||
] as const;
|
] as const;
|
||||||
export type AgentRole = (typeof AGENT_ROLES)[number];
|
export type AgentRole = (typeof AGENT_ROLES)[number];
|
||||||
|
|
||||||
|
export const AGENT_ROLE_LABELS: Record<AgentRole, string> = {
|
||||||
|
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 = [
|
export const AGENT_ICON_NAMES = [
|
||||||
"bot",
|
"bot",
|
||||||
"cpu",
|
"cpu",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export {
|
|||||||
AGENT_STATUSES,
|
AGENT_STATUSES,
|
||||||
AGENT_ADAPTER_TYPES,
|
AGENT_ADAPTER_TYPES,
|
||||||
AGENT_ROLES,
|
AGENT_ROLES,
|
||||||
|
AGENT_ROLE_LABELS,
|
||||||
AGENT_ICON_NAMES,
|
AGENT_ICON_NAMES,
|
||||||
ISSUE_STATUSES,
|
ISSUE_STATUSES,
|
||||||
ISSUE_PRIORITIES,
|
ISSUE_PRIORITIES,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const serverScript = mode === "watch" ? "dev:watch" : "dev";
|
|||||||
const child = spawn(
|
const child = spawn(
|
||||||
pnpmBin,
|
pnpmBin,
|
||||||
["--filter", "@paperclipai/server", serverScript, ...forwardedArgs],
|
["--filter", "@paperclipai/server", serverScript, ...forwardedArgs],
|
||||||
{ stdio: "inherit", env },
|
{ stdio: "inherit", env, shell: process.platform === "win32" },
|
||||||
);
|
);
|
||||||
|
|
||||||
child.on("exit", (code, signal) => {
|
child.on("exit", (code, signal) => {
|
||||||
|
|||||||
@@ -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 secret = process.env.BETTER_AUTH_SECRET ?? process.env.PAPERCLIP_AGENT_JWT_SECRET ?? "paperclip-dev-secret";
|
||||||
const effectiveTrustedOrigins = trustedOrigins ?? deriveAuthTrustedOrigins(config);
|
const effectiveTrustedOrigins = trustedOrigins ?? deriveAuthTrustedOrigins(config);
|
||||||
|
|
||||||
|
const publicUrl = process.env.PAPERCLIP_PUBLIC_URL ?? baseUrl;
|
||||||
|
const isHttpOnly = publicUrl ? publicUrl.startsWith("http://") : false;
|
||||||
|
|
||||||
const authConfig = {
|
const authConfig = {
|
||||||
baseURL: baseUrl,
|
baseURL: baseUrl,
|
||||||
secret,
|
secret,
|
||||||
@@ -86,7 +89,9 @@ export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins?
|
|||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
requireEmailVerification: false,
|
requireEmailVerification: false,
|
||||||
|
disableSignUp: config.authDisableSignUp,
|
||||||
},
|
},
|
||||||
|
...(isHttpOnly ? { advanced: { useSecureCookies: false } } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface Config {
|
|||||||
allowedHostnames: string[];
|
allowedHostnames: string[];
|
||||||
authBaseUrlMode: AuthBaseUrlMode;
|
authBaseUrlMode: AuthBaseUrlMode;
|
||||||
authPublicBaseUrl: string | undefined;
|
authPublicBaseUrl: string | undefined;
|
||||||
|
authDisableSignUp: boolean;
|
||||||
databaseMode: DatabaseMode;
|
databaseMode: DatabaseMode;
|
||||||
databaseUrl: string | undefined;
|
databaseUrl: string | undefined;
|
||||||
embeddedPostgresDataDir: string;
|
embeddedPostgresDataDir: string;
|
||||||
@@ -142,6 +143,11 @@ export function loadConfig(): Config {
|
|||||||
authBaseUrlModeFromEnv ??
|
authBaseUrlModeFromEnv ??
|
||||||
fileConfig?.auth?.baseUrlMode ??
|
fileConfig?.auth?.baseUrlMode ??
|
||||||
(authPublicBaseUrl ? "explicit" : "auto");
|
(authPublicBaseUrl ? "explicit" : "auto");
|
||||||
|
const disableSignUpFromEnv = process.env.PAPERCLIP_AUTH_DISABLE_SIGN_UP;
|
||||||
|
const authDisableSignUp: boolean =
|
||||||
|
disableSignUpFromEnv !== undefined
|
||||||
|
? disableSignUpFromEnv === "true"
|
||||||
|
: (fileConfig?.auth?.disableSignUp ?? false);
|
||||||
const allowedHostnamesFromEnvRaw = process.env.PAPERCLIP_ALLOWED_HOSTNAMES;
|
const allowedHostnamesFromEnvRaw = process.env.PAPERCLIP_ALLOWED_HOSTNAMES;
|
||||||
const allowedHostnamesFromEnv = allowedHostnamesFromEnvRaw
|
const allowedHostnamesFromEnv = allowedHostnamesFromEnvRaw
|
||||||
? allowedHostnamesFromEnvRaw
|
? allowedHostnamesFromEnvRaw
|
||||||
@@ -203,6 +209,7 @@ export function loadConfig(): Config {
|
|||||||
allowedHostnames,
|
allowedHostnames,
|
||||||
authBaseUrlMode,
|
authBaseUrlMode,
|
||||||
authPublicBaseUrl,
|
authPublicBaseUrl,
|
||||||
|
authDisableSignUp,
|
||||||
databaseMode: fileDatabaseMode,
|
databaseMode: fileDatabaseMode,
|
||||||
databaseUrl: process.env.DATABASE_URL ?? fileDbUrl,
|
databaseUrl: process.env.DATABASE_URL ?? fileDbUrl,
|
||||||
embeddedPostgresDataDir: resolveHomeAwarePath(
|
embeddedPostgresDataDir: resolveHomeAwarePath(
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export function agentRoutes(db: Db) {
|
|||||||
adapterConfig: Record<string, unknown>,
|
adapterConfig: Record<string, unknown>,
|
||||||
) {
|
) {
|
||||||
if (adapterType !== "opencode_local") return;
|
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) ?? {};
|
const runtimeEnv = asRecord(runtimeConfig.env) ?? {};
|
||||||
try {
|
try {
|
||||||
await ensureOpenCodeModelConfiguredAndAvailable({
|
await ensureOpenCodeModelConfiguredAndAvailable({
|
||||||
@@ -420,7 +420,7 @@ export function agentRoutes(db: Db) {
|
|||||||
inputAdapterConfig,
|
inputAdapterConfig,
|
||||||
{ strictMode: strictSecretsMode },
|
{ strictMode: strictSecretsMode },
|
||||||
);
|
);
|
||||||
const runtimeAdapterConfig = await secretsSvc.resolveAdapterConfigForRuntime(
|
const { config: runtimeAdapterConfig } = await secretsSvc.resolveAdapterConfigForRuntime(
|
||||||
companyId,
|
companyId,
|
||||||
normalizedAdapterConfig,
|
normalizedAdapterConfig,
|
||||||
);
|
);
|
||||||
@@ -1264,7 +1264,7 @@ export function agentRoutes(db: Db) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = asRecord(agent.adapterConfig) ?? {};
|
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({
|
const result = await runClaudeLogin({
|
||||||
runId: `claude-login-${randomUUID()}`,
|
runId: `claude-login-${randomUUID()}`,
|
||||||
agent: {
|
agent: {
|
||||||
|
|||||||
@@ -575,6 +575,10 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const assigneeChanged = assigneeWillChange;
|
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.
|
// Merge all wakeups from this update into one enqueue per agent to avoid duplicate runs.
|
||||||
void (async () => {
|
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) {
|
if (commentBody && comment) {
|
||||||
let mentionedIds: string[] = [];
|
let mentionedIds: string[] = [];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -341,13 +341,17 @@ export function agentService(db: Db) {
|
|||||||
await ensureManager(companyId, data.reportsTo);
|
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 role = data.role ?? "general";
|
||||||
const normalizedPermissions = normalizeAgentPermissions(data.permissions, role);
|
const normalizedPermissions = normalizeAgentPermissions(data.permissions, role);
|
||||||
const created = await db
|
const created = await db
|
||||||
.insert(agents)
|
.insert(agents)
|
||||||
.values({ ...data, companyId, role, permissions: normalizedPermissions })
|
.values({ ...data, name: uniqueName, companyId, role, permissions: normalizedPermissions })
|
||||||
.returning()
|
.returning()
|
||||||
.then((rows) => rows[0]);
|
.then((rows) => rows[0]);
|
||||||
|
|
||||||
|
|||||||
@@ -1240,11 +1240,16 @@ export function heartbeatService(db: Db) {
|
|||||||
const mergedConfig = issueAssigneeOverrides?.adapterConfig
|
const mergedConfig = issueAssigneeOverrides?.adapterConfig
|
||||||
? { ...config, ...issueAssigneeOverrides.adapterConfig }
|
? { ...config, ...issueAssigneeOverrides.adapterConfig }
|
||||||
: config;
|
: config;
|
||||||
const resolvedConfig = await secretsSvc.resolveAdapterConfigForRuntime(
|
const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime(
|
||||||
agent.companyId,
|
agent.companyId,
|
||||||
mergedConfig,
|
mergedConfig,
|
||||||
);
|
);
|
||||||
const onAdapterMeta = async (meta: AdapterInvocationMeta) => {
|
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++, {
|
await appendRunEvent(currentRun, seq++, {
|
||||||
eventType: "adapter.invoke",
|
eventType: "adapter.invoke",
|
||||||
stream: "system",
|
stream: "system",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createReadStream, promises as fs } from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { notFound } from "../errors.js";
|
import { notFound } from "../errors.js";
|
||||||
|
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
|
||||||
|
|
||||||
export type RunLogStoreType = "local_file";
|
export type RunLogStoreType = "local_file";
|
||||||
|
|
||||||
@@ -148,7 +149,7 @@ let cachedStore: RunLogStore | null = null;
|
|||||||
|
|
||||||
export function getRunLogStore() {
|
export function getRunLogStore() {
|
||||||
if (cachedStore) return cachedStore;
|
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);
|
cachedStore = createLocalFileRunLogStore(basePath);
|
||||||
return cachedStore;
|
return cachedStore;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,10 +308,11 @@ export function secretService(db: Db) {
|
|||||||
return normalized;
|
return normalized;
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveEnvBindings: async (companyId: string, envValue: unknown) => {
|
resolveEnvBindings: async (companyId: string, envValue: unknown): Promise<{ env: Record<string, string>; secretKeys: Set<string> }> => {
|
||||||
const record = asRecord(envValue);
|
const record = asRecord(envValue);
|
||||||
if (!record) return {} as Record<string, string>;
|
if (!record) return { env: {} as Record<string, string>, secretKeys: new Set<string>() };
|
||||||
const resolved: Record<string, string> = {};
|
const resolved: Record<string, string> = {};
|
||||||
|
const secretKeys = new Set<string>();
|
||||||
|
|
||||||
for (const [key, rawBinding] of Object.entries(record)) {
|
for (const [key, rawBinding] of Object.entries(record)) {
|
||||||
if (!ENV_KEY_RE.test(key)) {
|
if (!ENV_KEY_RE.test(key)) {
|
||||||
@@ -326,20 +327,22 @@ export function secretService(db: Db) {
|
|||||||
resolved[key] = binding.value;
|
resolved[key] = binding.value;
|
||||||
} else {
|
} else {
|
||||||
resolved[key] = await resolveSecretValue(companyId, binding.secretId, binding.version);
|
resolved[key] = await resolveSecretValue(companyId, binding.secretId, binding.version);
|
||||||
|
secretKeys.add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resolved;
|
return { env: resolved, secretKeys };
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record<string, unknown>) => {
|
resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record<string, unknown>): Promise<{ config: Record<string, unknown>; secretKeys: Set<string> }> => {
|
||||||
const resolved = { ...adapterConfig };
|
const resolved = { ...adapterConfig };
|
||||||
|
const secretKeys = new Set<string>();
|
||||||
if (!Object.prototype.hasOwnProperty.call(adapterConfig, "env")) {
|
if (!Object.prototype.hasOwnProperty.call(adapterConfig, "env")) {
|
||||||
return resolved;
|
return { config: resolved, secretKeys };
|
||||||
}
|
}
|
||||||
const record = asRecord(adapterConfig.env);
|
const record = asRecord(adapterConfig.env);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
resolved.env = {};
|
resolved.env = {};
|
||||||
return resolved;
|
return { config: resolved, secretKeys };
|
||||||
}
|
}
|
||||||
const env: Record<string, string> = {};
|
const env: Record<string, string> = {};
|
||||||
for (const [key, rawBinding] of Object.entries(record)) {
|
for (const [key, rawBinding] of Object.entries(record)) {
|
||||||
@@ -355,10 +358,11 @@ export function secretService(db: Db) {
|
|||||||
env[key] = binding.value;
|
env[key] = binding.value;
|
||||||
} else {
|
} else {
|
||||||
env[key] = await resolveSecretValue(companyId, binding.secretId, binding.version);
|
env[key] = await resolveSecretValue(companyId, binding.secretId, binding.version);
|
||||||
|
secretKeys.add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolved.env = env;
|
resolved.env = env;
|
||||||
return resolved;
|
return { config: resolved, secretKeys };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
"promptTemplate",
|
"promptTemplate",
|
||||||
String(config.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 }}..."
|
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
|
||||||
contentClassName="min-h-[88px] text-sm font-mono"
|
contentClassName="min-h-[88px] text-sm font-mono"
|
||||||
imageUploadHandler={async (file) => {
|
imageUploadHandler={async (file) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link } from "@/lib/router";
|
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 { agentsApi } from "../api/agents";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
@@ -24,6 +24,8 @@ const adapterLabels: Record<string, string> = {
|
|||||||
http: "HTTP",
|
http: "HTTP",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const roleLabels = AGENT_ROLE_LABELS as Record<string, string>;
|
||||||
|
|
||||||
function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) {
|
function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 py-1.5">
|
<div className="flex items-center gap-3 py-1.5">
|
||||||
@@ -51,7 +53,7 @@ export function AgentProperties({ agent, runtimeState }: AgentPropertiesProps) {
|
|||||||
<StatusBadge status={agent.status} />
|
<StatusBadge status={agent.status} />
|
||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
<PropertyRow label="Role">
|
<PropertyRow label="Role">
|
||||||
<span className="text-sm">{agent.role}</span>
|
<span className="text-sm">{roleLabels[agent.role] ?? agent.role}</span>
|
||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
{agent.title && (
|
{agent.title && (
|
||||||
<PropertyRow label="Title">
|
<PropertyRow label="Title">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { HelpCircle, ChevronDown, ChevronRight } from "lucide-react";
|
import { HelpCircle, ChevronDown, ChevronRight } from "lucide-react";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
|
import { AGENT_ROLE_LABELS } from "@paperclipai/shared";
|
||||||
|
|
||||||
/* ---- Help text for (?) tooltips ---- */
|
/* ---- Help text for (?) tooltips ---- */
|
||||||
export const help: Record<string, string> = {
|
export const help: Record<string, string> = {
|
||||||
@@ -59,11 +60,7 @@ export const adapterLabels: Record<string, string> = {
|
|||||||
http: "HTTP",
|
http: "HTTP",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const roleLabels: Record<string, string> = {
|
export const roleLabels = AGENT_ROLE_LABELS as Record<string, string>;
|
||||||
ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO",
|
|
||||||
engineer: "Engineer", designer: "Designer", pm: "PM",
|
|
||||||
qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General",
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ---- Primitive components ---- */
|
/* ---- Primitive components ---- */
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { PageTabBar } from "../components/PageTabBar";
|
|||||||
import { Tabs } from "@/components/ui/tabs";
|
import { Tabs } from "@/components/ui/tabs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Bot, Plus, List, GitBranch, SlidersHorizontal } from "lucide-react";
|
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<string, string> = {
|
const adapterLabels: Record<string, string> = {
|
||||||
claude_local: "Claude",
|
claude_local: "Claude",
|
||||||
@@ -30,11 +30,7 @@ const adapterLabels: Record<string, string> = {
|
|||||||
http: "HTTP",
|
http: "HTTP",
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleLabels: Record<string, string> = {
|
const roleLabels = AGENT_ROLE_LABELS as Record<string, string>;
|
||||||
ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO",
|
|
||||||
engineer: "Engineer", designer: "Designer", pm: "PM",
|
|
||||||
qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General",
|
|
||||||
};
|
|
||||||
|
|
||||||
type FilterTab = "all" | "active" | "paused" | "error";
|
type FilterTab = "all" | "active" | "paused" | "error";
|
||||||
|
|
||||||
@@ -230,7 +226,7 @@ export function Agents() {
|
|||||||
<EntityRow
|
<EntityRow
|
||||||
key={agent.id}
|
key={agent.id}
|
||||||
title={agent.name}
|
title={agent.name}
|
||||||
subtitle={`${agent.role}${agent.title ? ` - ${agent.title}` : ""}`}
|
subtitle={`${roleLabels[agent.role] ?? agent.role}${agent.title ? ` - ${agent.title}` : ""}`}
|
||||||
to={agentUrl(agent)}
|
to={agentUrl(agent)}
|
||||||
leading={
|
leading={
|
||||||
<span className="relative flex h-2.5 w-2.5">
|
<span className="relative flex h-2.5 w-2.5">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { EmptyState } from "../components/EmptyState";
|
|||||||
import { PageSkeleton } from "../components/PageSkeleton";
|
import { PageSkeleton } from "../components/PageSkeleton";
|
||||||
import { AgentIcon } from "../components/AgentIconPicker";
|
import { AgentIcon } from "../components/AgentIconPicker";
|
||||||
import { Network } from "lucide-react";
|
import { Network } from "lucide-react";
|
||||||
import type { Agent } from "@paperclipai/shared";
|
import { AGENT_ROLE_LABELS, type Agent } from "@paperclipai/shared";
|
||||||
|
|
||||||
// Layout constants
|
// Layout constants
|
||||||
const CARD_W = 200;
|
const CARD_W = 200;
|
||||||
@@ -421,11 +421,7 @@ export function OrgChart() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleLabels: Record<string, string> = {
|
const roleLabels = AGENT_ROLE_LABELS as Record<string, string>;
|
||||||
ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO",
|
|
||||||
engineer: "Engineer", designer: "Designer", pm: "PM",
|
|
||||||
qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General",
|
|
||||||
};
|
|
||||||
|
|
||||||
function roleLabel(role: string): string {
|
function roleLabel(role: string): string {
|
||||||
return roleLabels[role] ?? role;
|
return roleLabels[role] ?? role;
|
||||||
|
|||||||
Reference in New Issue
Block a user