From ba1966f17c7922457e59d84d142f65e5ef5d1173 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 31 Mar 2026 05:02:38 +0000 Subject: [PATCH] feat: encrypted account-level secrets (#501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add encrypted account-level secrets with UI for adding API keys Adds AccountSecret model with AES-256-GCM encryption, API routes for CRUD, "Add secret" button in model costs section, and injects decrypted secrets into action env (YAML secrets take precedence). Co-Authored-By: Claude Opus 4.6 * feat: repo secrets, sidebar icons, lazy learnings history - add repo-level secrets with inheritance from org secrets - add icons to console sidebar sections - fix learnings history modal: lazy fetch with hover prefetch, strip content from list response, load content per-expansion Made-with: Cursor * add input validation bounds for secrets and fix client-side name filter Made-with: Cursor * refactor: migrate all client-side data fetching to TanStack Query Replace manual useState/useEffect/fetch patterns and the custom usePolling hook with useQuery, useInfiniteQuery, and useMutation across the entire frontend for consistent caching, background refetching, and reactive invalidation. - ActiveWorkflowRunsSection: useQuery + refetchInterval - WorkflowRunHistory: useInfiniteQuery + polling query - LearningsSection: useQuery per revision (lazy) - FlagsSettings: self-contained useQuery + useMutation - SecretsCard: useMutation for delete - AddWorkflowButton, VerifyWorkflowButton: useMutation - EmailSignupForm, email-waitlist: useMutation - providers.tsx: enable refetchOnWindowFocus - Delete usePolling.ts (no remaining consumers) Made-with: Cursor * address PR review: squash migrations, rename accountSecrets → dbSecrets Squash the two separate secrets migrations into a single migration. Rename the wire format field from accountSecrets to dbSecrets since it now carries merged account + repo secrets. Made-with: Cursor * fix: update proxyKeys.ts imports after cache.ts -> yes package migration Made-with: Cursor --------- Co-authored-by: Claude Opus 4.6 --- entry | 18 +++++++++++++++--- main.ts | 12 ++++++++++++ models.ts | 2 +- post | 2 +- utils/runContext.ts | 3 +++ utils/runContextData.ts | 2 ++ 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/entry b/entry index b0fc77c..5e00519 100755 --- a/entry +++ b/entry @@ -107721,7 +107721,7 @@ var providers = { }), google: provider({ displayName: "Google", - envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"], + envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"], models: { "gemini-pro": { displayName: "Gemini Pro", @@ -151108,7 +151108,8 @@ async function fetchRunContext(params) { }, apiToken: data.apiToken, oss: data.oss ?? false, - proxyModel: data.proxyModel + proxyModel: data.proxyModel, + dbSecrets: data.dbSecrets }; } catch { clearTimeout(timeoutId); @@ -151133,7 +151134,8 @@ async function resolveRunContextData(params) { repoSettings: runContext.settings, apiToken: runContext.apiToken, oss: runContext.oss, - proxyModel: runContext.proxyModel + proxyModel: runContext.proxyModel, + dbSecrets: runContext.dbSecrets }; } @@ -151457,6 +151459,16 @@ async function main() { const initialOctokit = createOctokit(jobToken); const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken }); timer.checkpoint("runContextData"); + if (runContext.dbSecrets) { + for (const [key, value2] of Object.entries(runContext.dbSecrets)) { + if (!process.env[key]) { + process.env[key] = value2; + core5.setSecret(value2); + } + } + const count = Object.keys(runContext.dbSecrets).length; + if (count > 0) log.info(`\xBB ${count} db secret(s) loaded`); + } const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings); toolState.model = payload.model; if (payload.event.trigger === "pull_request_synchronize") { diff --git a/main.ts b/main.ts index 6c15ed6..5bf02fb 100644 --- a/main.ts +++ b/main.ts @@ -169,6 +169,18 @@ export async function main(): Promise { const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken }); timer.checkpoint("runContextData"); + // inject account-level secrets into process.env (YAML secrets take precedence) + if (runContext.dbSecrets) { + for (const [key, value] of Object.entries(runContext.dbSecrets)) { + if (!process.env[key]) { + process.env[key] = value; + core.setSecret(value); + } + } + const count = Object.keys(runContext.dbSecrets).length; + if (count > 0) log.info(`» ${count} db secret(s) loaded`); + } + // resolve payload to determine shell permission const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings); toolState.model = payload.model; diff --git a/models.ts b/models.ts index 04a0a48..b488b45 100644 --- a/models.ts +++ b/models.ts @@ -93,7 +93,7 @@ export const providers = { }), google: provider({ displayName: "Google", - envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"], + envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"], models: { "gemini-pro": { displayName: "Gemini Pro", diff --git a/post b/post index 68ff738..666a3dd 100755 --- a/post +++ b/post @@ -37559,7 +37559,7 @@ var providers = { }), google: provider({ displayName: "Google", - envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"], + envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"], models: { "gemini-pro": { displayName: "Gemini Pro", diff --git a/utils/runContext.ts b/utils/runContext.ts index 8aef1ee..c939bbe 100644 --- a/utils/runContext.ts +++ b/utils/runContext.ts @@ -27,6 +27,7 @@ export interface RunContext { apiToken: string; oss: boolean; proxyModel?: string | undefined; + dbSecrets?: Record | undefined; } const defaultSettings: RepoSettings = { @@ -82,6 +83,7 @@ export async function fetchRunContext(params: { apiToken: string; oss?: boolean; proxyModel?: string; + dbSecrets?: Record; } | null; if (data === null) { @@ -100,6 +102,7 @@ export async function fetchRunContext(params: { apiToken: data.apiToken, oss: data.oss ?? false, proxyModel: data.proxyModel, + dbSecrets: data.dbSecrets, }; } catch { clearTimeout(timeoutId); diff --git a/utils/runContextData.ts b/utils/runContextData.ts index 0d7a7d6..e529cd8 100644 --- a/utils/runContextData.ts +++ b/utils/runContextData.ts @@ -14,6 +14,7 @@ export interface RunContextData { apiToken: string; oss: boolean; proxyModel?: string | undefined; + dbSecrets?: Record | undefined; } interface ResolveRunContextDataParams { @@ -46,5 +47,6 @@ export async function resolveRunContextData( apiToken: runContext.apiToken, oss: runContext.oss, proxyModel: runContext.proxyModel, + dbSecrets: runContext.dbSecrets, }; }