blob: c1ca8a60f4a31bc2485929bf61fcbac3e7680b9d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type {
ConnectionCatalogEntry,
ConnectionModelDiscoveryResult,
ConnectionTestErrorClass,
ConnectionTestSummary,
} from '@maka/core/runtime-policy';
import { parseRequestHeaders } from '@maka/core/runtime-policy';
import { PROVIDER_REGISTRY, providerFallbackModelIds } from '@maka/core/llm-connections';
import {
createConnectionEffectFetchTransport,
type ConnectionEffectFetchTransport,
type ConnectionEffectProxySnapshot,
} from '@maka/runtime/network/scoped-fetch-transport';
import { createRequestCustomizationFetch } from '@maka/runtime/request-customization-fetch';
import {
isOAuthSubscriptionProvider,
parseOAuthSubscriptionTokens,
} from '@maka/runtime/subscription-credentials';
import { runConnectionModelDiscoveryEffect } from '@maka/runtime/model-fetcher';
import { runConnectionTestEffect } from '@maka/runtime/test-connection';
import {
type ConnectionEffectErrorKind,
type ConnectionModelDiscoveryEffectOutcome,
type ConnectionTestEffectOutcome,
} from '@maka/runtime/connection-effect-outcome';
import { type ConnectionEffectFetchDependency } from '@maka/runtime/connection-effect-fetch';
import {
authenticateRuntimePolicyStoresWriter,
RuntimePolicyStoreError,
type BeginConnectionTestResult,
type BeginModelFetchResult,
type ConnectionEffectCompletionResult,
type ConnectionOnboardingTicket,
type RuntimePolicyStoresWriter,
} from '@maka/storage/runtime-policy-stores';
import type {
ConnectionEffectChangedDomain,
ConnectionEffectFailureClass,
ConnectionEffectRejectionReason,
ConnectionModelFetchInput,
ConnectionModelFetchResult,
ConnectionOnboardingVerifyInput,
ConnectionOnboardingSaveInput,
ConnectionOnboardingSaveResult,
ConnectionTestProjection,
ConnectionTestRunInput,
ConnectionTestRunResult,
OperationOutcome,
} from '../protocol/index.js';
import type { ConnectionEffectOperationHandlerMap } from './operation-dispatcher.js';
import type { HostOAuthExecutionAuthority } from './oauth-execution-authority.js';
import { RuntimePolicyActivationGate } from './runtime-policy-activation-gate.js';
import { toRuntimePolicyProxy } from './runtime-policy-proxy.js';
type ModelDiscoveryRunner = (
connection: ConnectionCatalogEntry,
apiKey: string,
options: ConnectionEffectFetchDependency,
) => Promise<ConnectionModelDiscoveryEffectOutcome>;
type ConnectionTestRunner = (
connection: ConnectionCatalogEntry,
apiKey: string,
options: ConnectionEffectFetchDependency,
modelId?: string,
) => Promise<ConnectionTestEffectOutcome>;
export interface HostConnectionEffectCoordinatorOptions {
readonly stores: RuntimePolicyStoresWriter;
readonly activation: RuntimePolicyActivationGate;
readonly oauthCredentials: Pick<HostOAuthExecutionAuthority, 'bind'>;
readonly onCommittedMutation?: () => void;
readonly now?: () => number;
readonly runModelDiscovery?: ModelDiscoveryRunner;
readonly runConnectionTest?: ConnectionTestRunner;
readonly createTransport?: (
proxy: ConnectionEffectProxySnapshot | null,
) => ConnectionEffectFetchTransport;
}
/** Runs provider I/O outside Storage lanes and conditionally commits canonical results. */
export class HostConnectionEffectCoordinator {
readonly handlers: ConnectionEffectOperationHandlerMap = {
'connection.onboarding.save': (input) => this.#saveOnboarding(input),
'connection.onboarding.verify': (input) => this.#verifyOnboarding(input),
'connection.models.fetch': (input) => this.#fetchModels(input),
'connection.test.run': (input) => this.#testConnection(input),
};
readonly #stores: RuntimePolicyStoresWriter;
readonly #activation: RuntimePolicyActivationGate;
readonly #oauthCredentials: Pick<HostOAuthExecutionAuthority, 'bind'>;
readonly #onCommittedMutation: () => void;
readonly #now: () => number;
readonly #runModelDiscovery: ModelDiscoveryRunner;
readonly #runConnectionTest: ConnectionTestRunner;
readonly #createTransport: (
proxy: ConnectionEffectProxySnapshot | null,
) => ConnectionEffectFetchTransport;
readonly #tails = new Map<string, Promise<void>>();
#accepting = true;
#closePromise: Promise<void> | undefined;
constructor(options: HostConnectionEffectCoordinatorOptions) {
this.#stores = authenticateRuntimePolicyStoresWriter(options.stores);
this.#activation = options.activation;
this.#oauthCredentials = options.oauthCredentials;
this.#onCommittedMutation = options.onCommittedMutation ?? (() => {});
this.#now = options.now ?? Date.now;
this.#runModelDiscovery = options.runModelDiscovery ?? runConnectionModelDiscoveryEffect;
this.#runConnectionTest = options.runConnectionTest ?? runConnectionTestEffect;
this.#createTransport = options.createTransport ?? createConnectionEffectFetchTransport;
}
beginDrain(): void {
this.#accepting = false;
}
close(): Promise<void> {
if (this.#closePromise) return this.#closePromise;
this.beginDrain();
this.#closePromise = Promise.all([...this.#tails.values()]).then(() => undefined);
return this.#closePromise;
}
#fetchModels(
input: ConnectionModelFetchInput,
): Promise<OperationOutcome<'connection.models.fetch'>> {
return this.#admit(input.connectionId, 'connection.models.fetch', async () => {
const prepared = await this.#stores.operations.beginModelFetch(input.connectionId);
if (prepared.kind !== 'ready') return preparationResult(prepared);
const effect = await this.#withTransport(prepared, (fetch, secret) =>
this.#runModelDiscovery(prepared.connection, secret, { fetch }),
);
if (!effect.ok || effect.models.length === 0) {
return {
kind: 'failed',
errorClass: effect.ok ? 'invalid_response' : effect.error.kind,
};
}
const result: ConnectionModelDiscoveryResult = {
models: effect.models,
source: 'fetched',
fetchedAt: this.#now(),
};
const completion = await this.#complete(() =>
this.#stores.operations.completeModelFetch(prepared.ticket, result),
);
return completion.kind === 'committed'
? {
kind: 'committed',
catalogRevision: completion.snapshot.revision,
connection: committedConnectionBasis(
completion.snapshot.connections,
prepared.connection.connectionId,
),
modelCount: result.models.length,
source: result.source,
fetchedAt: result.fetchedAt,
}
: projectSuperseded(completion);
});
}
#verifyOnboarding(
input: ConnectionOnboardingVerifyInput,
): Promise<OperationOutcome<'connection.onboarding.verify'>> {
// Existing targets share their connection lane with models.fetch. Create
// attempts share a provider lane until Storage assigns and commits the
// next authoritative identity.
const lane = onboardingLane(input);
return this.#admit(lane, 'connection.onboarding.verify', async () => {
const prepared = await this.#discoverOnboarding(input);
if (prepared.kind === 'empty') return { kind: 'failed', errorClass: 'invalid_response' };
return prepared.kind === 'ready' ? { kind: 'verified', models: prepared.models } : prepared;
});
}
#saveOnboarding(
input: ConnectionOnboardingSaveInput,
): Promise<OperationOutcome<'connection.onboarding.save'>> {
const lane = onboardingLane(input);
return this.#admit(lane, 'connection.onboarding.save', async () => {
const prepared = await this.#discoverOnboarding(input);
if (prepared.kind === 'empty') return { kind: 'rejected', reason: 'model_unavailable' };
if (prepared.kind !== 'ready') return prepared;
const available = new Set(prepared.models.map(({ id }) => id));
// An adoption caller has no model inventory before the Host performs this
// discovery. Empty therefore means "enable everything this operation
// verified"; ordinary onboarding callers may still submit an explicit
// non-empty subset.
const enabledModelIds =
input.enabledModelIds.length > 0
? input.enabledModelIds
: prepared.models.map(({ id }) => id);
if (enabledModelIds.some((modelId) => !available.has(modelId))) {
return { kind: 'rejected', reason: 'model_unavailable' };
}
return this.#activation.runMutation(async () =>
this.#commitOnboarding(enabledModelIds, prepared),
);
});
}
async #discoverOnboarding(input: ConnectionOnboardingVerifyInput): Promise<OnboardingDiscovery> {
// The begin/complete ticket pair binds this discovery to the connection
// revision, credential, and proxy it observed: a concurrent policy update
// between the remote probe and the commit supersedes the save instead of
// pairing the new endpoint with an inventory it never produced. Verify
// simply abandons its ticket (they are WeakMap-held one-shots).
const begun = await this.#stores.operations.beginConnectionOnboarding({
target: input.target,
baseUrl: input.baseUrl,
});
if (begun.kind === 'target_missing') {
// Identity supplied by the client names a connection that is gone or
// changed provider type: reject instead of deriving a duplicate.
return { kind: 'rejected', reason: 'connection_not_found' };
}
if (begun.kind === 'provider_unsupported') {
return { kind: 'rejected', reason: 'provider_unsupported' };
}
if (begun.kind === 'catalog_full') {
return { kind: 'rejected', reason: 'catalog_full' };
}
if (begun.kind === 'slug_taken') {
return { kind: 'rejected', reason: 'slug_taken' };
}
const providerType = begun.candidate.providerType;
const candidate = begun.existingConnection ?? undefined;
const supplied = input.apiKey?.trim() ?? '';
const persistedSecret = supplied || begun.storedSecret || '';
if (
(PROVIDER_REGISTRY[providerType].authKind === 'api_key' ||
PROVIDER_REGISTRY[providerType].authKind === 'oauth_token') &&
persistedSecret.length === 0
) {
return { kind: 'rejected', reason: 'credential_not_configured' };
}
let discoverySecret = persistedSecret;
if (isOAuthSubscriptionProvider(providerType) && persistedSecret.length > 0) {
const tokens = parseOAuthSubscriptionTokens(persistedSecret);
if (!tokens) return { kind: 'failed', errorClass: 'auth' };
discoverySecret = tokens.access_token;
}
// Mirrors the blank-key contract above: a null baseUrl reuses the
// existing connection's persisted endpoint or the registry default.
// A relay provider with no endpoint from any of those sources cannot
// run discovery — reject up front instead of probing an empty URL.
const base = candidate
? { ...candidate, ...(begun.baseUrl ? { baseUrl: begun.baseUrl } : {}) }
: transientConnection(begun.candidate, begun.baseUrl);
if (!base.baseUrl && !PROVIDER_REGISTRY[providerType].baseUrl) {
return { kind: 'rejected', reason: 'base_url_not_configured' };
}
// The ticket's basis certifies this exact proxy, so discovery must use
// the pinned value rather than re-resolving it (a flip-and-restore
// between the two reads would otherwise slip past the basis check).
if (begun.proxyCredentialMissing) return { kind: 'failed', errorClass: 'network' };
const transport = this.#createTransport(
toRuntimePolicyProxy(begun.networkProxy, begun.proxySecret ?? undefined),
);
try {
// The probe must go out the way the models path sends it (#withTransport):
// with the connection's custom request headers and body overlay, both
// pinned by the ticket whose basis the commit revalidates.
const effect = await this.#runModelDiscovery(base, discoverySecret, {
fetch: createRequestCustomizationFetch(transport.fetch, {
headers: begun.requestHeadersSecret
? parseRequestHeaders(begun.requestHeadersSecret)
: {},
bodyOverlay: base.requestBodyOverlay,
}),
});
if (!effect.ok) return { kind: 'failed', errorClass: effect.error.kind };
if (effect.models.length === 0) return { kind: 'empty' };
return {
kind: 'ready',
ticket: begun.ticket,
suppliedSecret: supplied,
models: effect.models,
};
} finally {
await transport.close();
}
}
async #commitOnboarding(
enabledModelIds: readonly string[],
prepared: Extract<OnboardingDiscovery, { readonly kind: 'ready' }>,
): Promise<ConnectionOnboardingSaveResult> {
try {
const committed = await this.#stores.operations.completeConnectionOnboarding(
prepared.ticket,
{
suppliedSecret: prepared.suppliedSecret || null,
enabledModelIds,
discovery: {
models: prepared.models,
source: 'fetched',
fetchedAt: this.#now(),
},
},
);
if (committed.kind === 'catalog_full') {
return { kind: 'rejected', reason: 'catalog_full' };
}
if (committed.kind === 'target_missing') {
return { kind: 'rejected', reason: 'connection_not_found' };
}
if (committed.kind === 'slug_taken') {
return { kind: 'rejected', reason: 'slug_taken' };
}
if (committed.kind === 'superseded') {
return { kind: 'rejected', reason: 'superseded' };
}
if (committed.changed) this.#onCommittedMutation();
return { kind: 'saved', connection: committed.connection };
} catch (error) {
if (error instanceof RuntimePolicyStoreError && error.code === 'commit_outcome_unknown') {
this.#onCommittedMutation();
}
throw error;
}
}
#testConnection(input: ConnectionTestRunInput): Promise<OperationOutcome<'connection.test.run'>> {
return this.#admit(input.connectionId, 'connection.test.run', async () => {
const prepared = await this.#stores.operations.beginConnectionTest(
input.connectionId,
input.modelId,
);
if (prepared.kind !== 'ready') return preparationResult(prepared);
const effect = await this.#withTransport(prepared, (fetch, secret) =>
this.#runConnectionTest(
prepared.connection,
secret,
{ fetch },
prepared.modelId ?? undefined,
),
);
const projected = projectConnectionTest(effect, this.#now());
const completion = await this.#complete(() =>
this.#stores.operations.completeConnectionTest(
prepared.ticket,
connectionTestSummary(projected),
),
);
return completion.kind === 'committed'
? {
kind: 'committed',
catalogRevision: completion.snapshot.revision,
connection: committedConnectionBasis(
completion.snapshot.connections,
prepared.connection.connectionId,
),
test: projected,
}
: projectSuperseded(completion);
});
}
#admit<
K extends
| 'connection.models.fetch'
| 'connection.test.run'
| 'connection.onboarding.verify'
| 'connection.onboarding.save',
>(
connectionId: string,
operation: K,
run: () => Promise<Extract<OperationOutcome<K>, { ok: true }>['result']>,
): Promise<OperationOutcome<K>> {
if (!this.#accepting) {
return Promise.resolve({
ok: false,
error: { code: 'host_draining', message: 'Runtime Host is draining' },
} as OperationOutcome<K>);
}
return this.#enqueue(connectionId, async () => {
try {
return { ok: true, result: await run() } as OperationOutcome<K>;
} catch (error) {
return storeFailure<K>(error);
}
});
}
async #withTransport<T>(
prepared: Pick<
BeginModelFetchReady | BeginConnectionTestReady,
'connection' | 'networkProxy' | 'secretMaterial'
>,
run: (fetch: typeof globalThis.fetch, secret: string) => Promise<T>,
): Promise<T> {
const proxy = toRuntimePolicyProxy(
prepared.networkProxy,
prepared.secretMaterial.networkProxy?.secret,
);
const secret = await this.#connectionSecret(prepared, proxy);
const transport = this.#createTransport(proxy);
try {
const requestHeaders = prepared.secretMaterial.requestHeaders
? parseRequestHeaders(prepared.secretMaterial.requestHeaders.secret)
: {};
return await run(
createRequestCustomizationFetch(transport.fetch, {
headers: requestHeaders,
bodyOverlay: prepared.connection.requestBodyOverlay,
}),
secret,
);
} finally {
await transport.close();
}
}
async #connectionSecret(
prepared: Pick<
BeginModelFetchReady | BeginConnectionTestReady,
'connection' | 'secretMaterial'
>,
proxy: ConnectionEffectProxySnapshot | null,
): Promise<string> {
const material = prepared.secretMaterial.connection;
if (!material) return '';
if (!isOAuthSubscriptionProvider(prepared.connection.providerType)) return material.secret;
const binding = this.#oauthCredentials.bind({
providerType: prepared.connection.providerType,
connectionId: prepared.connection.connectionId,
connectionSlug: prepared.connection.slug,
material,
createRefreshTransport: () => this.#createTransport(proxy),
});
return (await binding.resolve()).access_token;
}
async #complete(
commit: () => Promise<ConnectionEffectCompletionResult>,
): Promise<ConnectionEffectCompletionResult> {
return this.#activation.runMutation(async () => {
try {
const result = await commit();
if (result.kind === 'committed') this.#onCommittedMutation();
return result;
} catch (error) {
if (error instanceof RuntimePolicyStoreError && error.code === 'commit_outcome_unknown') {
this.#onCommittedMutation();
}
throw error;
}
});
}
#enqueue<T>(connectionId: string, run: () => Promise<T>): Promise<T> {
const previous = this.#tails.get(connectionId) ?? Promise.resolve();
const pending = previous.then(run, run);
const tail = pending.then(
() => undefined,
() => undefined,
);
this.#tails.set(connectionId, tail);
void tail.finally(() => {
if (this.#tails.get(connectionId) === tail) this.#tails.delete(connectionId);
});
return pending;
}
}
type BeginModelFetchReady = Extract<BeginModelFetchResult, { readonly kind: 'ready' }>;
type BeginConnectionTestReady = Extract<BeginConnectionTestResult, { readonly kind: 'ready' }>;
type OnboardingDiscovery =
| { readonly kind: 'empty' }
| {
readonly kind: 'ready';
readonly ticket: ConnectionOnboardingTicket;
readonly suppliedSecret: string;
readonly models: ConnectionModelDiscoveryResult['models'];
}
| {
readonly kind: 'rejected';
readonly reason:
| 'provider_unsupported'
| 'connection_not_found'
| 'credential_not_configured'
| 'base_url_not_configured'
| 'slug_taken'
| 'catalog_full';
}
| { readonly kind: 'failed'; readonly errorClass: ConnectionEffectFailureClass };
function preparationResult(
prepared: Exclude<BeginModelFetchResult, { readonly kind: 'ready' }>,
): Extract<ConnectionModelFetchResult, { readonly kind: 'rejected' }>;
function preparationResult(
prepared: Exclude<BeginConnectionTestResult, { readonly kind: 'ready' }>,
): Extract<ConnectionTestRunResult, { readonly kind: 'rejected' }>;
function preparationResult(
prepared: Exclude<BeginModelFetchResult | BeginConnectionTestResult, { readonly kind: 'ready' }>,
): Extract<ConnectionModelFetchResult, { readonly kind: 'rejected' }> {
return {
kind: 'rejected',
reason: prepared.kind satisfies ConnectionEffectRejectionReason,
};
}
function projectSuperseded(
completion: Extract<ConnectionEffectCompletionResult, { readonly kind: 'superseded' }>,
): Extract<ConnectionModelFetchResult | ConnectionTestRunResult, { readonly kind: 'superseded' }> {
return {
kind: 'superseded',
changed: completion.changed.map((domain) => domain satisfies ConnectionEffectChangedDomain),
};
}
function projectConnectionTest(
outcome: ConnectionTestEffectOutcome,
checkedAt: number,
): ConnectionTestProjection {
const checkedAtIso = new Date(checkedAt).toISOString();
if (outcome.ok) {
return {
kind: 'verified',
checkedAt: checkedAtIso,
modelId: outcome.modelId,
latencyMs: outcome.latencyMs,
};
}
return {
kind: 'failed',
checkedAt: checkedAtIso,
modelId: outcome.modelId ?? null,
latencyMs: outcome.latencyMs ?? null,
statusCode: outcome.error.statusCode ?? null,
errorClass: outcome.error.kind,
};
}
function connectionTestSummary(projection: ConnectionTestProjection): ConnectionTestSummary {
return projection.kind === 'verified'
? { status: 'verified', checkedAt: projection.checkedAt }
: {
status: projection.errorClass === 'auth' ? 'needs_reauth' : 'error',
checkedAt: projection.checkedAt,
errorClass: toConnectionTestErrorClass(projection.errorClass),
};
}
function toConnectionTestErrorClass(kind: ConnectionEffectErrorKind): ConnectionTestErrorClass {
return kind === 'invalid_response' ? 'unknown' : kind;
}
function committedConnectionBasis(
connections: readonly ConnectionCatalogEntry[],
connectionId: string,
) {
const connection = connections.find((candidate) => candidate.connectionId === connectionId);
if (!connection) throw new Error('Connection effect commit omitted its connection');
return { connectionId, revision: connection.revision };
}
function storeFailure<
K extends
| 'connection.models.fetch'
| 'connection.test.run'
| 'connection.onboarding.verify'
| 'connection.onboarding.save',
>(error: unknown): OperationOutcome<K> {
if (!(error instanceof RuntimePolicyStoreError)) throw error;
switch (error.code) {
case 'commit_outcome_unknown':
return operationFailure(
'commit_outcome_unknown',
'Connection effect commit outcome is unknown',
);
case 'io_failed':
case 'invalid_document':
return operationFailure('persistence_failed', 'Connection effect persistence failed');
case 'invalid_policy_input':
case 'invalid_connection_input':
case 'revision_conflict':
return operationFailure('invalid_request', 'Connection effect request is invalid');
case 'invalid_credential_input':
throw new Error('Connection effect admitted an invalid credential operation');
}
}
function operationFailure<
K extends
| 'connection.models.fetch'
| 'connection.test.run'
| 'connection.onboarding.verify'
| 'connection.onboarding.save',
>(
code: 'commit_outcome_unknown' | 'persistence_failed' | 'invalid_request',
message: string,
): OperationOutcome<K> {
return { ok: false, error: { code, message } } as OperationOutcome<K>;
}
function transientConnection(
identity: Pick<ConnectionCatalogEntry, 'connectionId' | 'slug' | 'providerType'>,
baseUrl: string | null = null,
): ConnectionCatalogEntry {
const { providerType } = identity;
const definition = PROVIDER_REGISTRY[providerType];
const models = providerFallbackModelIds(definition).map((id) => ({ id }));
return {
connectionId: identity.connectionId,
revision: 0,
slug: identity.slug,
name: definition.label,
providerType,
...((baseUrl ?? definition.baseUrl) ? { baseUrl: baseUrl ?? definition.baseUrl } : {}),
enabled: true,
enabledModelIds: models.map(({ id }) => id),
models,
modelSource: 'fallback',
modelsFetchedAt: 0,
};
}
function onboardingLane(input: ConnectionOnboardingVerifyInput): string {
return input.target.kind === 'existing'
? input.target.connectionId
: `onboarding:create:${input.target.providerType}`;
}