blob: 115f558736be951e78de2ee4a84e1fc9d9aa4565 [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 { stat } from 'node:fs/promises';
import {
deriveTaskSubmissionReadiness,
type TaskSubmissionReadinessDimension,
type TaskSubmissionReadinessSnapshot,
} from '@maka/core/task-submission-readiness';
import { PROVIDER_REGISTRY, type LlmConnection } from '@maka/core/llm-connections';
import { providerAuthRequiresSecret } from '@maka/core/llm-connections';
import type { ConnectionCatalogEntry, ConnectionCatalogSnapshot } from '@maka/core/runtime-policy';
import type { RuntimeHostConnection } from '@maka/runtime-host/client';
export interface RuntimeHostCliTaskReadinessInput {
readonly connection: RuntimeHostConnection;
readonly catalog: ConnectionCatalogSnapshot;
readonly cwd: string;
readonly connectionSlug?: string;
readonly model?: string;
readonly workspaceState?: 'ready' | 'missing' | 'unavailable' | 'unknown';
}
export async function readRuntimeHostCliTaskReadiness(
input: RuntimeHostCliTaskReadinessInput,
now: () => number = Date.now,
): Promise<TaskSubmissionReadinessSnapshot> {
const checkedAt = now();
const entry = resolveCatalogEntry(input.catalog, input.connectionSlug);
const modelTarget = entry
? {
kind: 'resolved' as const,
connection: catalogEntryAsLlmConnection(entry, input.catalog),
hasSecret: await readHasSecret(input.connection, entry),
requestedModel: input.model,
checkedAt,
}
: input.connectionSlug
? { kind: 'connection_missing' as const, connectionSlug: input.connectionSlug, checkedAt }
: { kind: 'missing_default' as const, checkedAt };
return deriveTaskSubmissionReadiness({
checkedAt,
runtime: { state: 'ready', checkedAt },
modelTarget,
workspace: { state: input.workspaceState ?? (await inspectWorkspace(input.cwd)), checkedAt },
});
}
export function isRuntimeHostCliTaskBlocked(snapshot: TaskSubmissionReadinessSnapshot): boolean {
return snapshot.state === 'repair_required' || snapshot.state === 'unavailable';
}
export function formatRuntimeHostCliTaskBlockers(
snapshot: TaskSubmissionReadinessSnapshot,
cliCommand = 'maka',
): string {
return snapshot.blockers
.filter((blocker) => blocker.state !== 'unknown')
.map((blocker) => `${blocker.blockerCode ?? blocker.id}: ${repairHint(blocker, cliCommand)}`)
.join('\n');
}
function resolveCatalogEntry(
catalog: ConnectionCatalogSnapshot,
requestedSlug: string | undefined,
): ConnectionCatalogEntry | undefined {
if (requestedSlug) {
return catalog.connections.find((candidate) => candidate.slug === requestedSlug);
}
return catalog.connections.find(
(candidate) => candidate.connectionId === catalog.defaultTarget?.connectionId,
);
}
function catalogEntryAsLlmConnection(
entry: ConnectionCatalogEntry,
catalog: ConnectionCatalogSnapshot,
): LlmConnection {
const defaultModel =
entry.connectionId === catalog.defaultTarget?.connectionId
? catalog.defaultTarget.modelId
: (entry.enabledModelIds[0] ?? '');
return {
slug: entry.slug,
name: entry.name,
providerType: entry.providerType,
...(entry.baseUrl ? { baseUrl: entry.baseUrl } : {}),
enabled: entry.enabled,
defaultModel,
enabledModelIds: [...entry.enabledModelIds],
models: [...entry.models],
...(entry.modelSource ? { modelSource: entry.modelSource } : {}),
...(entry.lastTest
? { lastTestStatus: entry.lastTest.status, lastTestAt: entry.lastTest.checkedAt }
: {}),
createdAt: 0,
updatedAt: 0,
};
}
async function readHasSecret(
connection: RuntimeHostConnection,
entry: ConnectionCatalogEntry,
): Promise<boolean | undefined> {
if (!providerAuthRequiresSecret(entry.providerType)) return false;
const authKind = PROVIDER_REGISTRY[entry.providerType].authKind;
const kind = authKind === 'oauth_token' ? 'oauth_token' : 'api_key';
try {
const result = await connection.request('credential.vault.query', {
locator: { scope: 'connection', connectionId: entry.connectionId, kind },
});
return result.kind === 'status' ? result.status.configured : undefined;
} catch {
return undefined;
}
}
async function inspectWorkspace(
cwd: string,
): Promise<'ready' | 'missing' | 'unavailable' | 'unknown'> {
try {
return (await stat(cwd)).isDirectory() ? 'ready' : 'unavailable';
} catch (error) {
const code = (error as NodeJS.ErrnoException | undefined)?.code;
if (code === 'ENOENT' || code === 'ENOTDIR') return 'missing';
if (code === 'EACCES') return 'unavailable';
return 'unknown';
}
}
function repairHint(blocker: TaskSubmissionReadinessDimension, cliCommand: string): string {
const target = blocker.repairTarget;
if (!target) return 'retry the command';
switch (target.kind) {
case 'provider_catalog':
return `run \`${cliCommand}\` to configure a model provider`;
case 'connection':
return `repair connection "${target.connectionSlug}" in \`${cliCommand}\``;
case 'models':
return 'choose an available connection with `--connection`';
case 'runtime_restart':
return 'restart Maka and retry';
case 'workspace_picker':
return 'choose an existing directory with `--cwd`';
case 'system_permission':
return `grant system permission "${target.permissionId}"`;
case 'capability_settings':
return `enable capability "${target.capabilityId}"`;
}
}