From 520299ce3d6e56d5735e0e1fab712770c08def98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 23 May 2026 19:54:02 -0400 Subject: [PATCH] Add provider auth lifecycle contracts --- .../src/main/services/provider-adapters.ts | 36 +++++++++++++++- .../main/services/provider-auth-lifecycle.ts | 43 +++++++++++++++++++ .../src/main/services/sdlc-service.ts | 6 ++- .../tests/contract/provider-contracts.test.ts | 11 +++++ 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 AgentCompany-New/src/main/services/provider-auth-lifecycle.ts diff --git a/AgentCompany-New/src/main/services/provider-adapters.ts b/AgentCompany-New/src/main/services/provider-adapters.ts index 866eeadc..0ce92470 100644 --- a/AgentCompany-New/src/main/services/provider-adapters.ts +++ b/AgentCompany-New/src/main/services/provider-adapters.ts @@ -1,6 +1,14 @@ import { PROVIDER_AUTH_TYPE, type ProviderAuthType } from "../../shared/domain"; import { listProviderModels, type ProviderModelEntry } from "../../shared/provider-models"; import type { ProviderObservedRateLimitState } from "../../shared/provider-runtime-domain"; +import { + authorizeWithParser, + refreshUnsupported, + revokeLocalProfile, + type ProviderAuthorizeHandler, + type ProviderRefreshHandler, + type ProviderRevokeHandler, +} from "./provider-auth-lifecycle"; import { parseProviderRateLimitHeaders } from "./provider-rate-limit-registry"; export interface ProviderExecutionInput { @@ -42,6 +50,9 @@ export interface ProviderRateLimitState { export interface ProviderAdapter { providerKey: string; authType: ProviderAuthType; + authorize: ProviderAuthorizeHandler; + refresh: ProviderRefreshHandler; + revoke: ProviderRevokeHandler; parseAuthProfile(secret: string): ProviderAuthProfile; listModels(ctx: ProviderContext): Promise; getRateLimitState(ctx: ProviderContext): Promise; @@ -86,6 +97,9 @@ export const providerAdapters: ProviderAdapterRegistry = { openai: { providerKey: "openai", authType: PROVIDER_AUTH_TYPE.API_KEY, + authorize: authorizeWithParser(parseApiKey), + refresh: refreshUnsupported("OpenAI API key"), + revoke: revokeLocalProfile((profile) => { requireApiKeyProfile(profile, "OpenAI"); }), parseAuthProfile: parseApiKey, listModels: () => listStaticModels("openai"), getRateLimitState: providerRateLimitUnknown, @@ -107,6 +121,9 @@ export const providerAdapters: ProviderAdapterRegistry = { anthropic: { providerKey: "anthropic", authType: PROVIDER_AUTH_TYPE.API_KEY, + authorize: authorizeWithParser(parseApiKey), + refresh: refreshUnsupported("Anthropic API key"), + revoke: revokeLocalProfile((profile) => { requireApiKeyProfile(profile, "Anthropic"); }), parseAuthProfile: parseApiKey, listModels: () => listStaticModels("anthropic"), getRateLimitState: providerRateLimitUnknown, @@ -152,6 +169,9 @@ export const providerAdapters: ProviderAdapterRegistry = { "azure-openai": { providerKey: "azure-openai", authType: PROVIDER_AUTH_TYPE.CLOUD, + authorize: authorizeWithParser(parseAzureProfile), + refresh: refreshUnsupported("Azure OpenAI cloud credential"), + revoke: revokeLocalProfile((profile) => { requireAzureProfile(profile); }), parseAuthProfile: parseAzureProfile, listModels: () => listStaticModels("azure-openai"), getRateLimitState: providerRateLimitUnknown, @@ -164,6 +184,9 @@ export const providerAdapters: ProviderAdapterRegistry = { "aws-bedrock": { providerKey: "aws-bedrock", authType: PROVIDER_AUTH_TYPE.CLOUD, + authorize: authorizeWithParser(parseBedrockProfile), + refresh: refreshUnsupported("AWS Bedrock cloud credential"), + revoke: revokeLocalProfile((profile) => { requireBedrockProfile(profile); }), parseAuthProfile: parseBedrockProfile, listModels: () => listStaticModels("aws-bedrock"), getRateLimitState: providerRateLimitUnknown, @@ -175,6 +198,9 @@ export const providerAdapters: ProviderAdapterRegistry = { "vertex-ai": { providerKey: "vertex-ai", authType: PROVIDER_AUTH_TYPE.CLOUD, + authorize: authorizeWithParser(parseVertexProfile), + refresh: refreshUnsupported("Vertex AI cloud credential"), + revoke: revokeLocalProfile((profile) => { requireVertexProfile(profile); }), parseAuthProfile: parseVertexProfile, listModels: () => listStaticModels("vertex-ai"), getRateLimitState: providerRateLimitUnknown, @@ -271,6 +297,9 @@ function openAiCompatibleAdapter({ return { providerKey, authType: PROVIDER_AUTH_TYPE.API_KEY, + authorize: authorizeWithParser(parseApiKey), + refresh: refreshUnsupported(`${label} API key`), + revoke: revokeLocalProfile((profile) => { requireApiKeyProfile(profile, label); }), parseAuthProfile: parseApiKey, listModels: () => listStaticModels(providerKey), getRateLimitState: providerRateLimitUnknown, @@ -301,10 +330,15 @@ async function checkOpenAiHealth(authProfile: ProviderAuthProfile, healthUrl: st } function browserTokenAdapter(providerKey: string, authType: ProviderAuthType): ProviderAdapter { + const parseAuthProfile = authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION ? parseAccountToken : parseAccessToken; + const assertProfile = authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION ? requireAccountTokenProfile : requireAccessTokenProfile; return { providerKey, authType, - parseAuthProfile: authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION ? parseAccountToken : parseAccessToken, + authorize: authorizeWithParser(parseAuthProfile), + refresh: refreshUnsupported(`${providerKey} token`), + revoke: revokeLocalProfile((profile) => { assertProfile(profile); }), + parseAuthProfile, listModels: () => listStaticModels(providerKey), getRateLimitState: providerRateLimitUnknown, checkHealth: async (input) => { diff --git a/AgentCompany-New/src/main/services/provider-auth-lifecycle.ts b/AgentCompany-New/src/main/services/provider-auth-lifecycle.ts new file mode 100644 index 00000000..554af7b4 --- /dev/null +++ b/AgentCompany-New/src/main/services/provider-auth-lifecycle.ts @@ -0,0 +1,43 @@ +import type { ProviderAuthProfile } from "./provider-adapters"; + +export interface ProviderAuthorizeInput { + secret: string; +} + +export interface ProviderAuthorizeResult { + authProfile: ProviderAuthProfile; +} + +export interface ProviderRefreshInput { + authProfile: ProviderAuthProfile; +} + +export interface ProviderRefreshResult { + authProfile: ProviderAuthProfile; + rotatedSecret: string | null; + refreshedAt: string; +} + +export interface ProviderRevokeInput { + authProfile: ProviderAuthProfile; +} + +export type ProviderAuthorizeHandler = (input: ProviderAuthorizeInput) => Promise; +export type ProviderRefreshHandler = (input: ProviderRefreshInput) => Promise; +export type ProviderRevokeHandler = (input: ProviderRevokeInput) => Promise; + +export function authorizeWithParser(parseAuthProfile: (secret: string) => ProviderAuthProfile): ProviderAuthorizeHandler { + return async (input) => ({ authProfile: parseAuthProfile(input.secret) }); +} + +export function refreshUnsupported(label: string): ProviderRefreshHandler { + return async () => { + throw new Error(`${label} does not support token refresh.`); + }; +} + +export function revokeLocalProfile(assertProfile: (profile: ProviderAuthProfile) => void): ProviderRevokeHandler { + return async (input) => { + assertProfile(input.authProfile); + }; +} diff --git a/AgentCompany-New/src/main/services/sdlc-service.ts b/AgentCompany-New/src/main/services/sdlc-service.ts index fe8bc1d5..d0c0095f 100644 --- a/AgentCompany-New/src/main/services/sdlc-service.ts +++ b/AgentCompany-New/src/main/services/sdlc-service.ts @@ -244,7 +244,11 @@ export class SdlcService { const missing = catalogKeys.filter((key) => !adapterKeys.includes(key)); const runtimeContractReady = PROVIDER_CATALOG.every((provider) => { const adapter = providerAdapters[provider.key]; - return typeof adapter.listModels === "function" && typeof adapter.getRateLimitState === "function"; + return typeof adapter.authorize === "function" + && typeof adapter.refresh === "function" + && typeof adapter.revoke === "function" + && typeof adapter.listModels === "function" + && typeof adapter.getRateLimitState === "function"; }); const schemaVersion = count(this.db, "select coalesce(max(version), 0) as count from schema_migrations"); const rateLimitRegistry = count(this.db, ` diff --git a/AgentCompany-New/tests/contract/provider-contracts.test.ts b/AgentCompany-New/tests/contract/provider-contracts.test.ts index 19094737..12e6f36f 100644 --- a/AgentCompany-New/tests/contract/provider-contracts.test.ts +++ b/AgentCompany-New/tests/contract/provider-contracts.test.ts @@ -90,6 +90,17 @@ describe("provider contracts", () => { } }); + test("exposes auth lifecycle adapter contracts", async () => { + for (const provider of PROVIDER_CATALOG) { + const adapter = providerAdapters[provider.key]; + const secret = PROVIDER_SECRET_SAMPLES[provider.key]; + const profile = adapter.parseAuthProfile(secret); + await expect(adapter.authorize({ secret })).resolves.toEqual({ authProfile: profile }); + await expect(adapter.revoke({ authProfile: profile })).resolves.toBeUndefined(); + await expect(adapter.refresh({ authProfile: profile })).rejects.toThrow("does not support token refresh"); + } + }); + test("runs the seeded contract eval suite", () => { const connection = createConnection(); const organization = new OrganizationService(connection.db);