mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 10:30:37 +09:00
Add project-level environment variables
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
97d4ce41b3
commit
8f23270f35
20 changed files with 13439 additions and 279 deletions
|
|
@ -27,6 +27,7 @@ import type {
|
|||
CompanyPortabilitySidebarOrder,
|
||||
CompanyPortabilitySkillManifestEntry,
|
||||
CompanySkill,
|
||||
AgentEnvConfig,
|
||||
RoutineVariable,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
|
|
@ -39,6 +40,7 @@ import {
|
|||
ROUTINE_TRIGGER_KINDS,
|
||||
ROUTINE_TRIGGER_SIGNING_MODES,
|
||||
deriveProjectUrlKey,
|
||||
envConfigSchema,
|
||||
normalizeAgentUrlKey,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
|
|
@ -387,6 +389,11 @@ function isSensitiveEnvKey(key: string) {
|
|||
);
|
||||
}
|
||||
|
||||
function normalizePortableProjectEnv(value: unknown): AgentEnvConfig | null {
|
||||
const parsed = envConfigSchema.safeParse(value);
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
type ResolvedSource = {
|
||||
manifest: CompanyPortabilityManifest;
|
||||
files: Record<string, CompanyPortabilityFileEntry>;
|
||||
|
|
@ -419,6 +426,7 @@ type ProjectLike = {
|
|||
targetDate: string | null;
|
||||
color: string | null;
|
||||
status: string;
|
||||
env: Record<string, unknown> | null;
|
||||
executionWorkspacePolicy: Record<string, unknown> | null;
|
||||
workspaces?: Array<{
|
||||
id: string;
|
||||
|
|
@ -2531,6 +2539,7 @@ function buildManifestFromPackageFiles(
|
|||
targetDate: asString(extension.targetDate),
|
||||
color: asString(extension.color),
|
||||
status: asString(extension.status),
|
||||
env: normalizePortableProjectEnv(extension.env),
|
||||
executionWorkspacePolicy: isPlainRecord(extension.executionWorkspacePolicy)
|
||||
? extension.executionWorkspacePolicy
|
||||
: null,
|
||||
|
|
@ -3159,6 +3168,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
|||
targetDate: project.targetDate ?? null,
|
||||
color: project.color ?? null,
|
||||
status: project.status,
|
||||
env: normalizePortableProjectEnv(project.env) ?? undefined,
|
||||
executionWorkspacePolicy: exportPortableProjectExecutionWorkspacePolicy(
|
||||
slug,
|
||||
project.executionWorkspacePolicy,
|
||||
|
|
@ -4095,6 +4105,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
|||
status: manifestProject.status && PROJECT_STATUSES.includes(manifestProject.status as any)
|
||||
? manifestProject.status as typeof PROJECT_STATUSES[number]
|
||||
: "backlog",
|
||||
env: manifestProject.env,
|
||||
executionWorkspacePolicy: stripPortableProjectExecutionWorkspaceRefs(manifestProject.executionWorkspacePolicy),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,36 @@ const SESSIONED_LOCAL_ADAPTERS = new Set([
|
|||
"pi_local",
|
||||
]);
|
||||
|
||||
type RuntimeConfigSecretResolver = Pick<
|
||||
ReturnType<typeof secretService>,
|
||||
"resolveAdapterConfigForRuntime" | "resolveEnvBindings"
|
||||
>;
|
||||
|
||||
export async function resolveExecutionRunAdapterConfig(input: {
|
||||
companyId: string;
|
||||
executionRunConfig: Record<string, unknown>;
|
||||
projectEnv: unknown;
|
||||
secretsSvc: RuntimeConfigSecretResolver;
|
||||
}) {
|
||||
const { config: resolvedConfig, secretKeys } = await input.secretsSvc.resolveAdapterConfigForRuntime(
|
||||
input.companyId,
|
||||
input.executionRunConfig,
|
||||
);
|
||||
const projectEnvResolution = input.projectEnv
|
||||
? await input.secretsSvc.resolveEnvBindings(input.companyId, input.projectEnv)
|
||||
: { env: {}, secretKeys: new Set<string>() };
|
||||
if (Object.keys(projectEnvResolution.env).length > 0) {
|
||||
resolvedConfig.env = {
|
||||
...parseObject(resolvedConfig.env),
|
||||
...projectEnvResolution.env,
|
||||
};
|
||||
for (const key of projectEnvResolution.secretKeys) {
|
||||
secretKeys.add(key);
|
||||
}
|
||||
}
|
||||
return { resolvedConfig, secretKeys };
|
||||
}
|
||||
|
||||
export function applyPersistedExecutionWorkspaceConfig(input: {
|
||||
config: Record<string, unknown>;
|
||||
workspaceConfig: ExecutionWorkspaceConfig | null;
|
||||
|
|
@ -2309,17 +2339,20 @@ export function heartbeatService(db: Db) {
|
|||
: null;
|
||||
const contextProjectId = readNonEmptyString(context.projectId);
|
||||
const executionProjectId = issueContext?.projectId ?? contextProjectId;
|
||||
const projectExecutionWorkspacePolicy = executionProjectId
|
||||
const projectContext = executionProjectId
|
||||
? await db
|
||||
.select({ executionWorkspacePolicy: projects.executionWorkspacePolicy })
|
||||
.select({
|
||||
executionWorkspacePolicy: projects.executionWorkspacePolicy,
|
||||
env: projects.env,
|
||||
})
|
||||
.from(projects)
|
||||
.where(and(eq(projects.id, executionProjectId), eq(projects.companyId, agent.companyId)))
|
||||
.then((rows) =>
|
||||
gateProjectExecutionWorkspacePolicy(
|
||||
parseProjectExecutionWorkspacePolicy(rows[0]?.executionWorkspacePolicy),
|
||||
isolatedWorkspacesEnabled,
|
||||
))
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: null;
|
||||
const projectExecutionWorkspacePolicy = gateProjectExecutionWorkspacePolicy(
|
||||
parseProjectExecutionWorkspacePolicy(projectContext?.executionWorkspacePolicy),
|
||||
isolatedWorkspacesEnabled,
|
||||
);
|
||||
const taskSession = taskKey
|
||||
? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey)
|
||||
: null;
|
||||
|
|
@ -2416,10 +2449,12 @@ export function heartbeatService(db: Db) {
|
|||
: persistedWorkspaceManagedConfig;
|
||||
const configSnapshot = buildExecutionWorkspaceConfigSnapshot(mergedConfig);
|
||||
const executionRunConfig = stripWorkspaceRuntimeFromExecutionRunConfig(mergedConfig);
|
||||
const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime(
|
||||
agent.companyId,
|
||||
const { resolvedConfig, secretKeys } = await resolveExecutionRunAdapterConfig({
|
||||
companyId: agent.companyId,
|
||||
executionRunConfig,
|
||||
);
|
||||
projectEnv: projectContext?.env ?? null,
|
||||
secretsSvc,
|
||||
});
|
||||
const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(agent.companyId);
|
||||
const runtimeConfig = {
|
||||
...resolvedConfig,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ function canonicalizeBinding(binding: EnvBinding): CanonicalEnvBinding {
|
|||
}
|
||||
|
||||
export function secretService(db: Db) {
|
||||
type NormalizeEnvOptions = {
|
||||
strictMode?: boolean;
|
||||
fieldPath?: string;
|
||||
};
|
||||
|
||||
async function getById(id: string) {
|
||||
return db
|
||||
.select()
|
||||
|
|
@ -94,10 +99,10 @@ export function secretService(db: Db) {
|
|||
async function normalizeEnvConfig(
|
||||
companyId: string,
|
||||
envValue: unknown,
|
||||
opts?: { strictMode?: boolean },
|
||||
opts?: NormalizeEnvOptions,
|
||||
): Promise<AgentEnvConfig> {
|
||||
const record = asRecord(envValue);
|
||||
if (!record) throw unprocessable("adapterConfig.env must be an object");
|
||||
if (!record) throw unprocessable(`${opts?.fieldPath ?? "env"} must be an object`);
|
||||
|
||||
const normalized: AgentEnvConfig = {};
|
||||
for (const [key, rawBinding] of Object.entries(record)) {
|
||||
|
|
@ -292,6 +297,12 @@ export function secretService(db: Db) {
|
|||
opts?: { strictMode?: boolean },
|
||||
) => normalizeAdapterConfigForPersistenceInternal(companyId, adapterConfig, opts),
|
||||
|
||||
normalizeEnvBindingsForPersistence: async (
|
||||
companyId: string,
|
||||
envValue: unknown,
|
||||
opts?: NormalizeEnvOptions,
|
||||
) => normalizeEnvConfig(companyId, envValue, opts),
|
||||
|
||||
normalizeHireApprovalPayloadForPersistence: async (
|
||||
companyId: string,
|
||||
payload: Record<string, unknown>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue