mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-01 15:40:27 +00:00
feat(core): add external credential references (#53624)
This commit is contained in:
parent
d5cf9c2548
commit
369aad308b
9 changed files with 60 additions and 16 deletions
|
|
@ -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<FormField>]
|
||||
|
||||
export type CredentialValue = CredentialOAuth | CredentialKey
|
||||
export type CredentialValue = CredentialOAuth | CredentialKey | CredentialExternal
|
||||
|
||||
export type FormFields2 = [FormField1, ...Array<FormField1>]
|
||||
|
||||
|
|
@ -5854,6 +5856,11 @@ export type CredentialCreateInput = {
|
|||
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
|
||||
}
|
||||
}
|
||||
| {
|
||||
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<string>
|
||||
}
|
||||
}
|
||||
| {
|
||||
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<string>
|
||||
}
|
||||
}
|
||||
| {
|
||||
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<string>
|
||||
}
|
||||
}
|
||||
| {
|
||||
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<string>
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly type: "external"
|
||||
readonly methodID: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
}
|
||||
readonly activate?: boolean
|
||||
}["activate"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export interface Interface extends State.Transformable<Editor> {
|
|||
readonly connection: {
|
||||
/** Returns the active connection for one integration. */
|
||||
readonly active: (id: ID) => Effect.Effect<IntegrationConnection.Info | undefined>
|
||||
/** 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<Credential.Value | undefined, AuthorizationError>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Manag
|
|||
}
|
||||
return yield* ctx.integration.connection.resolve(connection).pipe(
|
||||
Effect.flatMap((credential) => {
|
||||
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<HttpClient.HttpClient | Bus.Service | Manag
|
|||
return yield* Effect.fail(new Error("OpenCode Console connection changed"))
|
||||
}
|
||||
const credential = yield* ctx.integration.connection.resolve(active)
|
||||
if (!credential) return yield* Effect.fail(new Error("OpenCode Console is not connected"))
|
||||
if (!credential || credential.type === "external")
|
||||
return yield* Effect.fail(new Error("OpenCode Console is not connected"))
|
||||
const metadata = credential.metadata
|
||||
const orgID = typeof metadata?.orgID === "string" ? metadata.orgID : undefined
|
||||
const token = credential.type === "oauth" ? credential.access : credential.key
|
||||
|
|
@ -469,7 +470,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Manag
|
|||
}),
|
||||
})
|
||||
|
||||
function fetchConfig(http: HttpClient.HttpClient, value: Credential.Value) {
|
||||
function fetchConfig(http: HttpClient.HttpClient, value: Credential.Key | Credential.OAuth) {
|
||||
// Scoped so responses whose body is never read (404, errors) are released here instead of by a GC-time abort.
|
||||
return HttpClient.withScope(http)
|
||||
.execute(
|
||||
|
|
@ -532,7 +533,7 @@ function organizationName(credential: Credential.Value) {
|
|||
return typeof credential.metadata?.orgName === "string" ? credential.metadata.orgName : undefined
|
||||
}
|
||||
|
||||
function credentialHeaders(value: Credential.Value): Record<string, string> {
|
||||
function credentialHeaders(value: Credential.Key | Credential.OAuth): Record<string, string> {
|
||||
const orgID = value.metadata?.orgID
|
||||
return {
|
||||
authorization: `Bearer ${value.type === "oauth" ? value.access : value.key}`,
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof External> {}
|
||||
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<typeof Value>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue