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:
@@ -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,
|
||||
@@ -86,7 +89,9 @@ export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins?
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false,
|
||||
disableSignUp: config.authDisableSignUp,
|
||||
},
|
||||
...(isHttpOnly ? { advanced: { useSecureCookies: false } } : {}),
|
||||
};
|
||||
|
||||
if (!baseUrl) {
|
||||
|
||||
@@ -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,11 @@ export function loadConfig(): Config {
|
||||
authBaseUrlModeFromEnv ??
|
||||
fileConfig?.auth?.baseUrlMode ??
|
||||
(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 allowedHostnamesFromEnv = allowedHostnamesFromEnvRaw
|
||||
? allowedHostnamesFromEnvRaw
|
||||
@@ -203,6 +209,7 @@ export function loadConfig(): Config {
|
||||
allowedHostnames,
|
||||
authBaseUrlMode,
|
||||
authPublicBaseUrl,
|
||||
authDisableSignUp,
|
||||
databaseMode: fileDatabaseMode,
|
||||
databaseUrl: process.env.DATABASE_URL ?? fileDbUrl,
|
||||
embeddedPostgresDataDir: resolveHomeAwarePath(
|
||||
|
||||
@@ -245,7 +245,7 @@ export function agentRoutes(db: Db) {
|
||||
adapterConfig: Record<string, unknown>,
|
||||
) {
|
||||
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({
|
||||
@@ -420,7 +420,7 @@ export function agentRoutes(db: Db) {
|
||||
inputAdapterConfig,
|
||||
{ strictMode: strictSecretsMode },
|
||||
);
|
||||
const runtimeAdapterConfig = await secretsSvc.resolveAdapterConfigForRuntime(
|
||||
const { config: runtimeAdapterConfig } = await secretsSvc.resolveAdapterConfigForRuntime(
|
||||
companyId,
|
||||
normalizedAdapterConfig,
|
||||
);
|
||||
@@ -1264,7 +1264,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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, string>; secretKeys: Set<string> }> => {
|
||||
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 secretKeys = new Set<string>();
|
||||
|
||||
for (const [key, rawBinding] of Object.entries(record)) {
|
||||
if (!ENV_KEY_RE.test(key)) {
|
||||
@@ -326,20 +327,22 @@ 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<string, unknown>) => {
|
||||
resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record<string, unknown>): Promise<{ config: Record<string, unknown>; secretKeys: Set<string> }> => {
|
||||
const resolved = { ...adapterConfig };
|
||||
const secretKeys = new Set<string>();
|
||||
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<string, string> = {};
|
||||
for (const [key, rawBinding] of Object.entries(record)) {
|
||||
@@ -355,10 +358,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 };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user