forked from bai/curriculum-project-hub
feat: secure organization provider credentials
This commit is contained in:
+138
-32
@@ -1,22 +1,37 @@
|
||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { InMemoryModelRegistry, type ModelRegistry } from "../agent/models.js";
|
||||
import { lockActiveOrganization } from "../org/status.js";
|
||||
import { decryptStoredProviderCredential } from "../connections/providerConnections.js";
|
||||
import { openProviderProxyLease, type AgentProviderLease, type ProviderUpstreamCredential } from "../connections/providerProxy.js";
|
||||
import { LocalSecretEnvelope } from "../security/secretEnvelope.js";
|
||||
|
||||
export type Env = Readonly<Record<string, string | undefined>>;
|
||||
|
||||
type EnvSource = Env | (() => Env);
|
||||
|
||||
const DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api";
|
||||
const DEFAULT_SONNET_MODEL = "anthropic/claude-sonnet-5";
|
||||
const DEFAULT_SONNET_LABEL = "Claude Sonnet 5";
|
||||
const DEFAULT_AGENT_MAX_TURNS = 25;
|
||||
|
||||
export interface ProviderRuntimeSettings {
|
||||
readonly id: string;
|
||||
readonly baseUrl: string;
|
||||
readonly authToken: string;
|
||||
readonly anthropicApiKey: string;
|
||||
readonly sdkEnv: Record<string, string | undefined>;
|
||||
openAgentLease(input: { readonly runId: string }): Promise<AgentProviderLease>;
|
||||
}
|
||||
|
||||
export interface ProviderLeaseContext {
|
||||
readonly providerId: string;
|
||||
readonly projectId: string;
|
||||
readonly runId: string;
|
||||
}
|
||||
|
||||
export type ProviderLeaseFactory = (
|
||||
credential: ProviderUpstreamCredential,
|
||||
context: ProviderLeaseContext,
|
||||
) => Promise<AgentProviderLease>;
|
||||
|
||||
const DEFAULT_PROVIDER_LEASE_FACTORY: ProviderLeaseFactory = (credential) =>
|
||||
openProviderProxyLease(credential);
|
||||
|
||||
export interface RuntimeScope {
|
||||
readonly projectId?: string;
|
||||
}
|
||||
@@ -44,26 +59,11 @@ export class EnvRuntimeSettings implements RuntimeSettings {
|
||||
}
|
||||
|
||||
async provider(providerId: string, scope?: RuntimeScope): Promise<ProviderRuntimeSettings> {
|
||||
void providerId;
|
||||
void scope;
|
||||
if (providerId !== "openrouter") {
|
||||
throw new Error(`unknown provider runtime settings: ${providerId}`);
|
||||
}
|
||||
const env = this.envSource();
|
||||
const baseUrl = readEnv(env, "ANTHROPIC_BASE_URL") ?? DEFAULT_OPENROUTER_BASE_URL;
|
||||
const authToken = requireSetting(env, "ANTHROPIC_AUTH_TOKEN");
|
||||
const anthropicApiKey = readEnv(env, "ANTHROPIC_API_KEY") ?? "";
|
||||
|
||||
return {
|
||||
id: providerId,
|
||||
baseUrl,
|
||||
authToken,
|
||||
anthropicApiKey,
|
||||
sdkEnv: {
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
ANTHROPIC_AUTH_TOKEN: authToken,
|
||||
ANTHROPIC_API_KEY: anthropicApiKey,
|
||||
},
|
||||
};
|
||||
throw new Error(
|
||||
"process-global provider credentials are disabled; use the project-scoped provider resolver",
|
||||
);
|
||||
}
|
||||
|
||||
async modelRegistry(scope?: RuntimeScope): Promise<ModelRegistry> {
|
||||
@@ -79,10 +79,124 @@ export class EnvRuntimeSettings implements RuntimeSettings {
|
||||
}
|
||||
}
|
||||
|
||||
export class DatabaseRuntimeSettings implements RuntimeSettings {
|
||||
private readonly envSettings: EnvRuntimeSettings;
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaClient,
|
||||
private readonly secrets: LocalSecretEnvelope,
|
||||
env: EnvSource = process.env,
|
||||
private readonly leaseFactory: ProviderLeaseFactory = DEFAULT_PROVIDER_LEASE_FACTORY,
|
||||
) {
|
||||
this.envSettings = new EnvRuntimeSettings(env);
|
||||
}
|
||||
|
||||
async provider(providerId: string, scope?: RuntimeScope): Promise<ProviderRuntimeSettings> {
|
||||
const projectId = scope?.projectId?.trim();
|
||||
if (projectId === undefined || projectId === "") {
|
||||
throw new Error("projectId is required to resolve provider credentials");
|
||||
}
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await loadActiveProviderSecret(tx, projectId, providerId);
|
||||
});
|
||||
|
||||
return {
|
||||
id: providerId,
|
||||
openAgentLease: async ({ runId }) => this.prisma.$transaction(async (tx) => {
|
||||
const resolved = await loadActiveProviderSecret(tx, projectId, providerId);
|
||||
const secretVersion = resolved.connection.activeSecretVersion;
|
||||
const credential = decryptStoredProviderCredential(this.secrets, {
|
||||
organizationId: resolved.organizationId,
|
||||
connectionId: resolved.connection.id,
|
||||
providerId,
|
||||
secretVersionId: secretVersion.id,
|
||||
envelopeVersion: secretVersion.envelopeVersion,
|
||||
keyId: secretVersion.keyId,
|
||||
envelope: secretVersion.envelope,
|
||||
});
|
||||
return this.leaseFactory(
|
||||
{
|
||||
baseUrl: credential.baseUrl,
|
||||
authToken: credential.authToken,
|
||||
anthropicApiKey: credential.anthropicApiKey,
|
||||
},
|
||||
{ providerId, projectId, runId },
|
||||
);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
modelRegistry(scope?: RuntimeScope): Promise<ModelRegistry> {
|
||||
return this.envSettings.modelRegistry(scope);
|
||||
}
|
||||
|
||||
runPolicy(input: RunPolicyInput): Promise<RunPolicy> {
|
||||
return this.envSettings.runPolicy(input);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadActiveProviderSecret(
|
||||
tx: Prisma.TransactionClient,
|
||||
projectId: string,
|
||||
providerId: string,
|
||||
): Promise<{
|
||||
readonly organizationId: string;
|
||||
readonly connection: {
|
||||
readonly id: string;
|
||||
readonly activeSecretVersion: {
|
||||
readonly id: string;
|
||||
readonly connectionId: string;
|
||||
readonly envelopeVersion: number;
|
||||
readonly keyId: string;
|
||||
readonly envelope: Prisma.JsonValue;
|
||||
readonly retiredAt: Date | null;
|
||||
};
|
||||
};
|
||||
}> {
|
||||
const project = await tx.project.findUnique({
|
||||
where: { id: projectId },
|
||||
select: { organizationId: true, archivedAt: true },
|
||||
});
|
||||
if (project === null || project.archivedAt !== null) {
|
||||
throw new Error(`active project not found: ${projectId}`);
|
||||
}
|
||||
await lockActiveOrganization(tx, project.organizationId);
|
||||
|
||||
const connection = await tx.organizationProviderConnection.findUnique({
|
||||
where: {
|
||||
organizationId_providerId: {
|
||||
organizationId: project.organizationId,
|
||||
providerId,
|
||||
},
|
||||
},
|
||||
include: { activeSecretVersion: true },
|
||||
});
|
||||
const activeSecretVersion = connection?.activeSecretVersion;
|
||||
if (connection === null || connection.status !== "ACTIVE" ||
|
||||
activeSecretVersion === null || activeSecretVersion === undefined || activeSecretVersion.retiredAt !== null ||
|
||||
activeSecretVersion.connectionId !== connection.id) {
|
||||
throw new Error(`active provider connection not found: ${providerId} for project ${projectId}`);
|
||||
}
|
||||
return {
|
||||
organizationId: project.organizationId,
|
||||
connection: { id: connection.id, activeSecretVersion },
|
||||
};
|
||||
}
|
||||
|
||||
export function createEnvRuntimeSettings(env: EnvSource = process.env): RuntimeSettings {
|
||||
return new EnvRuntimeSettings(env);
|
||||
}
|
||||
|
||||
export function createDatabaseRuntimeSettings(
|
||||
prisma: PrismaClient,
|
||||
secrets: LocalSecretEnvelope,
|
||||
env: EnvSource = process.env,
|
||||
leaseFactory: ProviderLeaseFactory = DEFAULT_PROVIDER_LEASE_FACTORY,
|
||||
): RuntimeSettings {
|
||||
return new DatabaseRuntimeSettings(prisma, secrets, env, leaseFactory);
|
||||
}
|
||||
|
||||
export function defaultSonnetModel(env: Env = process.env): string {
|
||||
return readEnv(env, "ANTHROPIC_DEFAULT_SONNET_MODEL") ?? DEFAULT_SONNET_MODEL;
|
||||
}
|
||||
@@ -109,14 +223,6 @@ function readEnv(env: Env, name: string): string | undefined {
|
||||
return value === undefined || value === "" ? undefined : value;
|
||||
}
|
||||
|
||||
function requireSetting(env: Env, name: string): string {
|
||||
const value = readEnv(env, name);
|
||||
if (value === undefined) {
|
||||
throw new Error(`missing required runtime setting: ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveIntegerEnv(env: Env, name: string, fallback: number): number {
|
||||
const raw = readEnv(env, name);
|
||||
if (raw === undefined) return fallback;
|
||||
|
||||
Reference in New Issue
Block a user