From 369aad308bb1dd22d8845ccb8a8cb2f900d0c911 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:55:05 -0500 Subject: [PATCH] feat(core): add external credential references (#53624) --- .../client/src/promise/generated/types.ts | 31 +++++++++++++++++-- packages/core/src/credential.ts | 3 ++ packages/core/src/integration.ts | 4 +-- packages/core/src/model-resolver.ts | 2 +- packages/core/src/plugin/provider/azure.ts | 11 +++++-- packages/core/src/plugin/provider/gitlab.ts | 2 +- packages/core/src/plugin/provider/opencode.ts | 9 +++--- packages/schema/src/connection.ts | 4 +-- packages/schema/src/credential.ts | 10 +++++- 9 files changed, 60 insertions(+), 16 deletions(-) diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 2be14d396be5..39bb0fd07388 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -308,6 +308,8 @@ export type CredentialOAuth = { metadata?: { [x: string]: JsonValue } } +export type CredentialExternal = { type: "external"; methodID: string; metadata?: { [x: string]: JsonValue } } + export type ProjectVcs = string export type ProjectIcon = { url?: string; override?: string; color?: string } @@ -1492,7 +1494,7 @@ export type ConnectionCredentialInfo = { type: "credential" id: string label: string - method: "key" | "oauth" + method: "key" | "oauth" | "external" status?: ConnectionStatus } @@ -2256,7 +2258,7 @@ export type SessionMessageAssistantTool1 = { export type FormFields = [FormField, ...Array] -export type CredentialValue = CredentialOAuth | CredentialKey +export type CredentialValue = CredentialOAuth | CredentialKey | CredentialExternal export type FormFields2 = [FormField1, ...Array] @@ -5854,6 +5856,11 @@ export type CredentialCreateInput = { readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray } } + | { + readonly type: "external" + readonly methodID: string + readonly metadata?: { readonly [x: string]: JsonValue } + } readonly activate?: boolean }["id"] readonly integrationID: { @@ -5877,6 +5884,11 @@ export type CredentialCreateInput = { readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray } } + | { + readonly type: "external" + readonly methodID: string + readonly metadata?: { readonly [x: string]: JsonValue } + } readonly activate?: boolean }["integrationID"] readonly label?: { @@ -5900,6 +5912,11 @@ export type CredentialCreateInput = { readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray } } + | { + readonly type: "external" + readonly methodID: string + readonly metadata?: { readonly [x: string]: JsonValue } + } readonly activate?: boolean }["label"] readonly value: { @@ -5923,6 +5940,11 @@ export type CredentialCreateInput = { readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray } } + | { + readonly type: "external" + readonly methodID: string + readonly metadata?: { readonly [x: string]: JsonValue } + } readonly activate?: boolean }["value"] readonly activate?: { @@ -5946,6 +5968,11 @@ export type CredentialCreateInput = { readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray } } + | { + readonly type: "external" + readonly methodID: string + readonly metadata?: { readonly [x: string]: JsonValue } + } readonly activate?: boolean }["activate"] } diff --git a/packages/core/src/credential.ts b/packages/core/src/credential.ts index c6323614b6fa..f39940931afc 100644 --- a/packages/core/src/credential.ts +++ b/packages/core/src/credential.ts @@ -19,6 +19,9 @@ export type OAuth = Credential.OAuth export const Key = Credential.Key export type Key = Credential.Key +export const External = Credential.External +export type External = Credential.External + export const Value = Credential.Value export type Value = Credential.Value diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 39904eaed1b8..b03b1d4f28fa 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -165,7 +165,7 @@ export interface Interface extends State.Transformable { readonly connection: { /** Returns the active connection for one integration. */ readonly active: (id: ID) => Effect.Effect - /** Resolves a connection into usable credential material. */ + /** Resolves a connection into credential material or an external credential-source reference. */ readonly resolve: ( connection: IntegrationConnection.Info, ) => Effect.Effect @@ -700,7 +700,7 @@ const layer = Layer.effect( } const credential = yield* credentials.get(connection.id) if (!credential) return undefined - if (credential.value.type === "key") return credential.value + if (credential.value.type !== "oauth") return credential.value const implementation = state .get() .integrations.get(credential.integrationID) diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index 448b18d8b2aa..990fbad79633 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -306,7 +306,7 @@ function unresolvedProviderVariables(model: RuntimeInfo, baseURL: string) { } const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => { - if (!credential) return {} + if (!credential || credential.type === "external") return {} if (credential.type === "key") return { apiKey: credential.key } if (specifier === "@opencode/ai/providers/anthropic" || specifier === "@opencode/ai/providers/anthropic-compatible") return { authToken: credential.access } diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index 40cdeb015161..d3942fd64007 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -235,7 +235,7 @@ export function make( const resourceDeployments = Effect.fn("AzurePlugin.resourceDeployments")(function* ( url: string, - credential: Credential.Value, + credential: Credential.Key | Credential.OAuth, ) { return yield* http .execute( @@ -266,7 +266,7 @@ export function make( // data-plane version 2022-12-01 has it; later versions dropped `/deployments` and keep `/models`, which lists // models the resource can deploy rather than its deployments. // https://github.com/Azure/azure-rest-api-specs/blob/main/specification/cognitiveservices/data-plane/OpenAIAuthoring/stable/2022-12-01/azureopenai.json - const deployments = (url: string, resource: string, credential: Credential.Value) => + const deployments = (url: string, resource: string, credential: Credential.Key | Credential.OAuth) => credential.type === "oauth" ? managementDeployments(resource).pipe(Effect.catch(() => resourceDeployments(url, credential))) : resourceDeployments(url, credential) @@ -308,7 +308,12 @@ export function make( const credential = yield* ctx.integration.connection .resolve(connection) .pipe(Effect.orElseSucceed(() => undefined)) - if (!credential || (credential.type === "oauth" && credential.methodID !== methodID)) return + if ( + !credential || + credential.type === "external" || + (credential.type === "oauth" && credential.methodID !== methodID) + ) + return const found = yield* deployments(url, name, credential).pipe( // Azure promises no order; normalize it so a reordered response does not rebuild the model list. Effect.map((list) => list.toSorted((a, b) => a.name.localeCompare(b.name))), diff --git a/packages/core/src/plugin/provider/gitlab.ts b/packages/core/src/plugin/provider/gitlab.ts index 4f50815434d2..45652ec88194 100644 --- a/packages/core/src/plugin/provider/gitlab.ts +++ b/packages/core/src/plugin/provider/gitlab.ts @@ -69,7 +69,7 @@ export const GitLabPlugin = define({ const credential = stored ? yield* ctx.integration.connection.resolve(stored).pipe(Effect.orElseSucceed(() => undefined)) : undefined - if (!stored || !credential) { + if (!stored || !credential || credential.type === "external") { loaded.models = undefined loaded.connection = undefined return diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index da7ea2cd0d77..1ec0908870c0 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -201,7 +201,7 @@ export const OpencodePlugin = define { - if (!credential) + if (!credential || credential.type === "external") return Effect.succeed({ config: undefined, connection, organization: undefined, mcp: undefined }) return fetchConfig(http, credential).pipe( Effect.map((config) => ({ @@ -360,7 +360,8 @@ export const OpencodePlugin = define { +function credentialHeaders(value: Credential.Key | Credential.OAuth): Record { const orgID = value.metadata?.orgID return { authorization: `Bearer ${value.type === "oauth" ? value.access : value.key}`, diff --git a/packages/schema/src/connection.ts b/packages/schema/src/connection.ts index bf024c4531f6..6eeaaef4f5c3 100644 --- a/packages/schema/src/connection.ts +++ b/packages/schema/src/connection.ts @@ -20,8 +20,8 @@ export const CredentialInfo = Schema.Struct({ type: Schema.Literal("credential"), id: Credential.ID, label: Schema.String, - /** How the credential was obtained: a stored key or an OAuth grant. */ - method: Schema.Literals(["key", "oauth"]), + /** Whether the connection stores a key, an OAuth grant, or an external credential-source reference. */ + method: Schema.Literals(["key", "oauth", "external"]), status: optional(Status), }).annotate({ identifier: "Connection.CredentialInfo" }) diff --git a/packages/schema/src/credential.ts b/packages/schema/src/credential.ts index 408ca68e8ac0..859a2b17945e 100644 --- a/packages/schema/src/credential.ts +++ b/packages/schema/src/credential.ts @@ -46,7 +46,15 @@ export const Key = Schema.Struct({ configuration: optional(Form.Answer), }).annotate({ identifier: "Credential.Key" }) -export const Value = Schema.Union([OAuth, Key]) +/** References a credential source whose secrets and renewal are managed outside the credential store. */ +export interface External extends Schema.Schema.Type {} +export const External = Schema.Struct({ + type: Schema.Literal("external"), + methodID: IntegrationMethodID, + metadata: optional(Schema.Record(Schema.String, Schema.Unknown)), +}).annotate({ identifier: "Credential.External" }) + +export const Value = Schema.Union([OAuth, Key, External]) .pipe(Schema.toTaggedUnion("type")) .annotate({ identifier: "Credential.Value" }) export type Value = Schema.Schema.Type