feat(cli): self-provision pinned uv and defer MCP Python runtime install (#297)

Fixes a production crash-loop (PostHog issue 019eb68e): ktx mcp start
--foreground on a uv-less container eagerly installed the managed Python
runtime at boot, failed, and was restarted by its supervisor every ~62s
(122 exceptions from one install).

- MCP server factory now wires a lazy semantic-layer compute port that
  defers the runtime install to the first call, mirroring the already-lazy
  SQL-analysis port; the server boots and serves non-Python tools without
  the runtime.
- ktx no longer requires uv on PATH: it downloads its own pinned,
  sha256-verified uv build under the runtime root (KTX_RUNTIME_ROOT aware),
  always musl-static on Linux. PATH uv is never consulted.
- uv is acquired before the version dir is wiped, so a failed download
  cannot destroy an existing runtime.
- Acquisition failures (offline, intercepted download, unsupported
  platform) throw KtxExpectedError and stay out of Error Tracking; a
  missing binary inside a checksum-verified archive remains a plain Error.
- scripts/refresh-uv-manifest.mjs regenerates the pinned manifest
  (packages/cli/src/managed-uv-release.ts) on uv bumps.
- Setup consent prompt now discloses the uv download; docs updated.
This commit is contained in:
Andrey Avtomonov 2026-06-12 18:31:06 +02:00 committed by GitHub
parent 663eaff940
commit feb0818444
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 731 additions and 72 deletions

View file

@ -48,6 +48,11 @@ directory. Use it from any directory to generate editor or agent schema files.
| `stop` | Stop the **ktx** daemon | | `stop` | Stop the **ktx** daemon |
| `status` | Show managed Python runtime status and readiness checks | | `status` | Show managed Python runtime status and readiness checks |
`install` is self-contained: **ktx** downloads its own pinned, checksum-verified
`uv` build under the runtime root and uses it to provision Python and the
runtime wheel. Nothing needs to be installed on `PATH` first; the host only
needs network access to `github.com` during the first install.
## `admin runtime` Options ## `admin runtime` Options
| Flag | Description | Default | | Flag | Description | Default |

View file

@ -68,3 +68,4 @@ hosts and origins for browser clients.
| No **ktx** project found | Current directory has no `ktx.yaml` and `KTX_PROJECT_DIR` is unset | Run from a **ktx** project or pass `--project-dir <path>` | | No **ktx** project found | Current directory has no `ktx.yaml` and `KTX_PROJECT_DIR` is unset | Run from a **ktx** project or pass `--project-dir <path>` |
| Non-loopback host rejected | The server needs token auth before binding beyond localhost | Pass `--token <token>` or set `KTX_MCP_TOKEN` | | Non-loopback host rejected | The server needs token auth before binding beyond localhost | Pass `--token <token>` or set `KTX_MCP_TOKEN` |
| Client cannot connect | Host, port, token, allowed host, or allowed origin does not match the client | Check `ktx mcp status`, then restart with explicit `--host`, `--port`, `--allowed-host`, and `--allowed-origin` values | | Client cannot connect | Host, port, token, allowed host, or allowed origin does not match the client | Check `ktx mcp status`, then restart with explicit `--host`, `--port`, `--allowed-host`, and `--allowed-origin` values |
| A Python-backed tool reports a runtime install failure | A tool that needs the managed Python runtime (metric compute, query-history SQL analysis) ran on a host that cannot reach `github.com` to download the pinned `uv` and Python | The server still starts and serves catalog and search tools. Restore network access and retry, or pre-build the runtime where network is available: `ktx admin runtime install --yes` |

View file

@ -62,7 +62,7 @@ export function managedRuntimeInstallCommand(feature: KtxRuntimeFeature): string
function installPrompt(feature: KtxRuntimeFeature): string { function installPrompt(feature: KtxRuntimeFeature): string {
const label = feature === 'local-embeddings' ? 'local embeddings Python runtime' : 'core Python runtime'; const label = feature === 'local-embeddings' ? 'local embeddings Python runtime' : 'core Python runtime';
return `ktx needs to install the ${label}. This downloads Python dependencies with uv. Continue?`; return `ktx needs to install the ${label}. This downloads a pinned, checksum-verified uv build, Python, and dependencies. Continue?`;
} }
function runtimeRequiredMessage(feature: KtxRuntimeFeature): string { function runtimeRequiredMessage(feature: KtxRuntimeFeature): string {
@ -144,3 +144,32 @@ export async function createManagedPythonSemanticLayerComputePort(
...(projectId ? { projectId } : {}), ...(projectId ? { projectId } : {}),
}); });
} }
/**
* Defers the managed-runtime install to the first semantic-layer call so a
* long-lived server (the MCP server) can start and serve context tools that
* need no Python even when uv is absent. Caches on success only, so a runtime
* installed mid-session is picked up on the next call.
*/
export function createLazyManagedPythonSemanticLayerComputePort(
options: ManagedPythonSemanticLayerComputeOptions,
): KtxSemanticLayerComputePort {
let cached: KtxSemanticLayerComputePort | undefined;
const resolve = async (): Promise<KtxSemanticLayerComputePort> => {
if (!cached) {
cached = await createManagedPythonSemanticLayerComputePort(options);
}
return cached;
};
return {
async query(input) {
return (await resolve()).query(input);
},
async validateSources(input) {
return (await resolve()).validateSources(input);
},
async generateSources(input) {
return (await resolve()).generateSources(input);
},
};
}

View file

@ -1,12 +1,19 @@
import { execFile } from 'node:child_process'; import { execFile } from 'node:child_process';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { access, appendFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { access, appendFile, chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import { basename, join } from 'node:path'; import { basename, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { strFromU8, unzipSync } from 'fflate'; import { gunzipSync, strFromU8, unzipSync } from 'fflate';
import { z } from 'zod'; import { z } from 'zod';
import { KtxExpectedError } from './errors.js';
import {
MANAGED_UV_ARTIFACTS,
MANAGED_UV_VERSION,
type ManagedUvArtifact,
type ManagedUvPlatformKey,
} from './managed-uv-release.js';
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
@ -96,6 +103,7 @@ export interface ManagedPythonRuntimeInstallOptions extends ManagedPythonRuntime
features: KtxRuntimeFeature[]; features: KtxRuntimeFeature[];
force?: boolean; force?: boolean;
exec?: ManagedPythonRuntimeExec; exec?: ManagedPythonRuntimeExec;
fetchUvArtifact?: ManagedUvFetchArtifact;
} }
export interface ManagedPythonRuntimeInstallResult { export interface ManagedPythonRuntimeInstallResult {
@ -122,9 +130,29 @@ export interface ManagedPythonRuntimeDoctorCheck {
fix?: string; fix?: string;
} }
export type ManagedUvFetchArtifact = (url: string) => Promise<Uint8Array>;
/** @internal */ /** @internal */
export const MISSING_UV_RUNTIME_INSTALL_MESSAGE = export interface ManagedUvRelease {
'uv is required to install the ktx Python runtime. ktx does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx admin runtime install --yes'; version: string;
artifacts: Partial<Record<ManagedUvPlatformKey, ManagedUvArtifact>>;
}
const PINNED_UV_RELEASE: ManagedUvRelease = {
version: MANAGED_UV_VERSION,
artifacts: MANAGED_UV_ARTIFACTS,
};
/** @internal */
export interface EnsureManagedUvOptions {
platform?: NodeJS.Platform;
arch?: string;
env?: NodeJS.ProcessEnv;
homeDir?: string;
runtimeRoot?: string;
fetchArtifact?: ManagedUvFetchArtifact;
release?: ManagedUvRelease;
}
function defaultAssetDir(): string { function defaultAssetDir(): string {
return fileURLToPath(new URL('../assets/python/', import.meta.url)); return fileURLToPath(new URL('../assets/python/', import.meta.url));
@ -347,12 +375,145 @@ function managedRuntimeUvEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
return { ...baseEnv, UV_NO_CONFIG: '1' }; return { ...baseEnv, UV_NO_CONFIG: '1' };
} }
async function ensureUv(exec: ManagedPythonRuntimeExec, env?: NodeJS.ProcessEnv): Promise<string> { function managedUvBinaryName(platform: NodeJS.Platform): string {
return platform === 'win32' ? 'uv.exe' : 'uv';
}
/** @internal */
export function managedUvPath(options: EnsureManagedUvOptions = {}): string {
const platform = options.platform ?? process.platform;
const env = options.env ?? process.env;
const homeDir = options.homeDir ?? homedir();
const runtimeRoot = options.runtimeRoot ?? runtimeRootFor({ env, homeDir });
const version = (options.release ?? PINNED_UV_RELEASE).version;
return join(runtimeRoot, 'uv', version, managedUvBinaryName(platform));
}
async function defaultFetchUvArtifact(url: string): Promise<Uint8Array> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return new Uint8Array(await response.arrayBuffer());
}
function readTarField(block: Uint8Array, start: number, length: number): string {
const field = block.subarray(start, start + length);
const end = field.indexOf(0);
return strFromU8(end < 0 ? field : field.subarray(0, end));
}
function findTarEntry(archive: Uint8Array, matches: (name: string) => boolean): Uint8Array | undefined {
let offset = 0;
while (offset + 512 <= archive.length) {
const block = archive.subarray(offset, offset + 512);
const name = readTarField(block, 0, 100);
if (!name) {
return undefined;
}
const size = Number.parseInt(readTarField(block, 124, 12).trim() || '0', 8);
if (matches(name)) {
return archive.subarray(offset + 512, offset + 512 + size);
}
offset += 512 + Math.ceil(size / 512) * 512;
}
return undefined;
}
function extractUvFromArchive(input: { file: string; contents: Uint8Array; binaryName: string }): Uint8Array {
const entry = input.file.endsWith('.zip')
? unzipSync(input.contents)[input.binaryName]
: findTarEntry(gunzipSync(input.contents), (name) => name === input.binaryName || name.endsWith(`/${input.binaryName}`));
if (!entry) {
throw new Error(`uv archive ${input.file} is missing the ${input.binaryName} binary`);
}
return entry;
}
/**
* ktx provisions its own pinned uv under the runtime root; uv on PATH is never
* consulted, so runtime installs behave identically on every machine. All
* failures here are environment outcomes (offline host, intercepting proxy,
* unsupported platform) and stay out of Error Tracking via KtxExpectedError
* except a pin/layout mismatch inside a checksum-verified archive, which is a
* ktx release fault and must reach Error Tracking.
* @internal
*/
export async function ensureManagedUv(options: EnsureManagedUvOptions = {}): Promise<string> {
const platform = options.platform ?? process.platform;
const arch = options.arch ?? process.arch;
const release = options.release ?? PINNED_UV_RELEASE;
const binaryName = managedUvBinaryName(platform);
const uvPath = managedUvPath(options);
if (await pathExists(uvPath)) {
return uvPath;
}
const artifact = release.artifacts[`${platform}-${arch}` as ManagedUvPlatformKey];
if (!artifact) {
throw new KtxExpectedError(
`ktx does not bundle uv for ${platform}-${arch}. Place a uv ${release.version} binary at ${uvPath} and retry: ktx admin runtime install --yes`,
);
}
const url = `https://github.com/astral-sh/uv/releases/download/${release.version}/${artifact.file}`;
let contents: Uint8Array;
try { try {
const result = await exec('uv', ['--version'], { env }); contents = await (options.fetchArtifact ?? defaultFetchUvArtifact)(url);
return result.stdout.trim() || 'uv available'; } catch (error) {
} catch { throw new KtxExpectedError(
throw new Error(MISSING_UV_RUNTIME_INSTALL_MESSAGE); `ktx could not download uv ${release.version} (required to install the ktx Python runtime). ` +
'Check network access to github.com and retry: ktx admin runtime install --yes. ' +
`Air-gapped hosts: place the uv binary at ${uvPath}.`,
{ cause: error },
);
}
const sha256 = createHash('sha256').update(contents).digest('hex');
if (sha256 !== artifact.sha256) {
throw new KtxExpectedError(
`Downloaded uv ${release.version} failed checksum verification (a proxy or captive portal may have altered the download). Retry: ktx admin runtime install --yes`,
);
}
const binary = extractUvFromArchive({ file: artifact.file, contents, binaryName });
await mkdir(dirname(uvPath), { recursive: true });
const stagedPath = `${uvPath}.${process.pid}.download`;
await writeFile(stagedPath, binary);
await chmod(stagedPath, 0o755);
try {
await rename(stagedPath, uvPath);
} catch (error) {
// On Windows a concurrent install may have won the rename; the binary at
// uvPath is checksum-pinned identical, so reuse it.
await rm(stagedPath, { force: true });
if (!(await pathExists(uvPath))) {
throw error;
}
}
return uvPath;
}
async function ensureUv(input: {
exec: ManagedPythonRuntimeExec;
uvEnv: NodeJS.ProcessEnv;
options: ManagedPythonRuntimeLayoutOptions & { fetchUvArtifact?: ManagedUvFetchArtifact };
}): Promise<{ uvPath: string; version: string }> {
const uvPath = await ensureManagedUv({
platform: input.options.platform,
env: input.options.env,
homeDir: input.options.homeDir,
runtimeRoot: input.options.runtimeRoot,
fetchArtifact: input.options.fetchUvArtifact,
});
try {
const result = await input.exec(uvPath, ['--version'], { env: input.uvEnv });
return { uvPath, version: result.stdout.trim() || `uv ${MANAGED_UV_VERSION}` };
} catch (error) {
throw new KtxExpectedError(
`Managed uv at ${uvPath} failed to run. Delete it and retry: ktx admin runtime install --yes`,
{ cause: error },
);
} }
} }
@ -377,21 +538,23 @@ export async function installManagedPythonRuntime(
return { status: 'ready', layout, asset, manifest: existing }; return { status: 'ready', layout, asset, manifest: existing };
} }
// uv is acquired before the version dir is wiped, so a failed acquisition
// never destroys a previously installed runtime.
const { uvPath } = await ensureUv({ exec, uvEnv, options });
await rm(layout.versionDir, { recursive: true, force: true }); await rm(layout.versionDir, { recursive: true, force: true });
await mkdir(layout.versionDir, { recursive: true }); await mkdir(layout.versionDir, { recursive: true });
await writeFile(layout.installLogPath, ''); await writeFile(layout.installLogPath, '');
await ensureUv(exec, uvEnv);
await runLogged({ await runLogged({
exec, exec,
logPath: layout.installLogPath, logPath: layout.installLogPath,
command: 'uv', command: uvPath,
args: ['python', 'install', asset.requiresPython.minimumVersion], args: ['python', 'install', asset.requiresPython.minimumVersion],
env: uvEnv, env: uvEnv,
}); });
await runLogged({ await runLogged({
exec, exec,
logPath: layout.installLogPath, logPath: layout.installLogPath,
command: 'uv', command: uvPath,
args: ['venv', '--python', asset.requiresPython.minimumVersion, layout.venvDir], args: ['venv', '--python', asset.requiresPython.minimumVersion, layout.venvDir],
env: uvEnv, env: uvEnv,
}); });
@ -399,7 +562,7 @@ export async function installManagedPythonRuntime(
await runLogged({ await runLogged({
exec, exec,
logPath: layout.installLogPath, logPath: layout.installLogPath,
command: 'uv', command: uvPath,
args: ['pip', 'install', '--python', layout.pythonPath, wheelSpec], args: ['pip', 'install', '--python', layout.pythonPath, wheelSpec],
env: uvEnv, env: uvEnv,
}); });
@ -462,20 +625,20 @@ function check(
} }
export async function doctorManagedPythonRuntime( export async function doctorManagedPythonRuntime(
options: ManagedPythonRuntimeLayoutOptions & { exec?: ManagedPythonRuntimeExec }, options: ManagedPythonRuntimeLayoutOptions & { exec?: ManagedPythonRuntimeExec; fetchUvArtifact?: ManagedUvFetchArtifact },
): Promise<ManagedPythonRuntimeDoctorCheck[]> { ): Promise<ManagedPythonRuntimeDoctorCheck[]> {
const exec = options.exec ?? defaultExec; const exec = options.exec ?? defaultExec;
const checks: ManagedPythonRuntimeDoctorCheck[] = []; const checks: ManagedPythonRuntimeDoctorCheck[] = [];
try { try {
const version = await ensureUv(exec, managedRuntimeUvEnv(options.env ?? process.env)); const uv = await ensureUv({ exec, uvEnv: managedRuntimeUvEnv(options.env ?? process.env), options });
checks.push(check('pass', { id: 'uv', label: 'uv', detail: version })); checks.push(check('pass', { id: 'uv', label: 'uv', detail: `${uv.version} (managed: ${uv.uvPath})` }));
} catch (error) { } catch (error) {
checks.push( checks.push(
check('fail', { check('fail', {
id: 'uv', id: 'uv',
label: 'uv', label: 'uv',
detail: error instanceof Error ? error.message : String(error), detail: error instanceof Error ? error.message : String(error),
fix: 'Install uv, make sure it is on PATH, and run: ktx admin runtime install --yes', fix: 'Check network access to github.com and run: ktx admin runtime install --yes',
}), }),
); );
} }

View file

@ -0,0 +1,26 @@
// Generated by scripts/refresh-uv-manifest.mjs. Do not edit by hand.
// Regenerate with: node scripts/refresh-uv-manifest.mjs [<uv-version>]
export type ManagedUvPlatformKey =
| 'darwin-arm64'
| 'darwin-x64'
| 'linux-arm64'
| 'linux-x64'
| 'win32-arm64'
| 'win32-x64';
export interface ManagedUvArtifact {
file: string;
sha256: string;
}
export const MANAGED_UV_VERSION = '0.11.21';
export const MANAGED_UV_ARTIFACTS: Record<ManagedUvPlatformKey, ManagedUvArtifact> = {
'darwin-arm64': { file: 'uv-aarch64-apple-darwin.tar.gz', sha256: '1f921d491ba5ffeea774eb04d6681ecee379101341cbb1500394993b541bf3f4' }, // pragma: allowlist secret
'darwin-x64': { file: 'uv-x86_64-apple-darwin.tar.gz', sha256: 'f3c8e5708a84b920c18b691214d54d2b0da6b984789caae95d47c95120cb7765' }, // pragma: allowlist secret
'linux-arm64': { file: 'uv-aarch64-unknown-linux-musl.tar.gz', sha256: 'e71badaed2a2c3a404a0a00974b51c7ed5f5bc7be947916846005b739c68a5a2' }, // pragma: allowlist secret
'linux-x64': { file: 'uv-x86_64-unknown-linux-musl.tar.gz', sha256: '9dadff5b9e7b1d2d011e41852a1cbca713d9d5d88194f2eb6bd240fa4fb0a719' }, // pragma: allowlist secret
'win32-arm64': { file: 'uv-aarch64-pc-windows-msvc.zip', sha256: '74e443f8004022dde57a1bd0d10c097830f9ea8feb4ec927db52cd5d805c2f48' }, // pragma: allowlist secret
'win32-x64': { file: 'uv-x86_64-pc-windows-msvc.zip', sha256: 'ace861f360c6de2babedc1607d0f454b6b09a820dbc8182dc15af927e4df9589' }, // pragma: allowlist secret
};

View file

@ -8,7 +8,7 @@ import type { KtxCliIo } from './cli-runtime.js';
import { resolveProjectEmbeddingProvider } from './embedding-resolution.js'; import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js'; import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js';
import { createKtxCliScanConnector } from './local-scan-connectors.js'; import { createKtxCliScanConnector } from './local-scan-connectors.js';
import { createManagedPythonSemanticLayerComputePort } from './managed-python-command.js'; import { createLazyManagedPythonSemanticLayerComputePort } from './managed-python-command.js';
import { createManagedDaemonSqlAnalysisPort } from './managed-python-http.js'; import { createManagedDaemonSqlAnalysisPort } from './managed-python-http.js';
function noopMcpIo(): KtxCliIo { function noopMcpIo(): KtxCliIo {
@ -26,7 +26,7 @@ export async function createKtxMcpServerFactory(input: {
}): Promise<() => McpServer> { }): Promise<() => McpServer> {
const io = input.io ?? noopMcpIo(); const io = input.io ?? noopMcpIo();
const queryExecutor = createKtxCliIngestQueryExecutor(input.project); const queryExecutor = createKtxCliIngestQueryExecutor(input.project);
const semanticLayerCompute = await createManagedPythonSemanticLayerComputePort({ const semanticLayerCompute = createLazyManagedPythonSemanticLayerComputePort({
cliVersion: input.cliVersion, cliVersion: input.cliVersion,
installPolicy: 'auto', installPolicy: 'auto',
io, io,

View file

@ -1,5 +1,7 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { KtxExpectedError } from '../src/errors.js';
import { import {
createLazyManagedPythonSemanticLayerComputePort,
createManagedPythonSemanticLayerComputePort, createManagedPythonSemanticLayerComputePort,
ensureManagedPythonCommandRuntime, ensureManagedPythonCommandRuntime,
managedRuntimeInstallCommand, managedRuntimeInstallCommand,
@ -274,7 +276,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
}); });
expect(confirmInstall).toHaveBeenCalledWith( expect(confirmInstall).toHaveBeenCalledWith(
'ktx needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?', 'ktx needs to install the core Python runtime. This downloads a pinned, checksum-verified uv build, Python, and dependencies. Continue?',
io.io, io.io,
); );
expect(installRuntime).toHaveBeenCalledWith({ expect(installRuntime).toHaveBeenCalledWith({
@ -306,7 +308,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
).resolves.toBe(compute); ).resolves.toBe(compute);
expect(confirmInstall).toHaveBeenCalledWith( expect(confirmInstall).toHaveBeenCalledWith(
'ktx needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?', 'ktx needs to install the core Python runtime. This downloads a pinned, checksum-verified uv build, Python, and dependencies. Continue?',
io.io, io.io,
); );
expect(events).toContainEqual('start:Installing ktx Python runtime (core) with uv...'); expect(events).toContainEqual('start:Installing ktx Python runtime (core) with uv...');
@ -328,3 +330,95 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
).rejects.toThrow('ktx Python runtime installation was cancelled'); ).rejects.toThrow('ktx Python runtime installation was cancelled');
}); });
}); });
describe('createLazyManagedPythonSemanticLayerComputePort', () => {
it('does not touch the managed runtime at construction, so a server starts without uv', async () => {
const io = makeIo();
const readStatus = vi.fn(async () => missingStatus());
const installRuntime = vi.fn(async (): Promise<ManagedPythonRuntimeInstallResult> => {
throw new KtxExpectedError('uv missing');
});
const createPythonCompute = vi.fn(() => ({ query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() }));
const port = createLazyManagedPythonSemanticLayerComputePort({
cliVersion: '0.2.0',
installPolicy: 'auto',
io: io.io,
readStatus,
installRuntime,
createPythonCompute,
});
expect(readStatus).not.toHaveBeenCalled();
expect(installRuntime).not.toHaveBeenCalled();
expect(createPythonCompute).not.toHaveBeenCalled();
await expect(port.query({ sources: [], query: {} as never, dialect: 'postgres' })).rejects.toBeInstanceOf(
KtxExpectedError,
);
expect(installRuntime).toHaveBeenCalledTimes(1);
});
it('resolves the runtime once and reuses it across calls', async () => {
const io = makeIo();
const readStatus = vi.fn(async () => readyStatus());
const compute = {
query: vi.fn(),
validateSources: vi.fn(),
generateSources: vi.fn(),
};
const createPythonCompute = vi.fn(() => compute);
const port = createLazyManagedPythonSemanticLayerComputePort({
cliVersion: '0.2.0',
installPolicy: 'never',
io: io.io,
readStatus,
installRuntime: vi.fn(),
createPythonCompute,
});
await port.query({ sources: [], query: {} as never, dialect: 'postgres' });
await port.validateSources({ sources: [], dialect: 'postgres' });
expect(readStatus).toHaveBeenCalledTimes(1);
expect(createPythonCompute).toHaveBeenCalledTimes(1);
expect(compute.query).toHaveBeenCalledTimes(1);
expect(compute.validateSources).toHaveBeenCalledTimes(1);
});
it('retries the runtime resolution after a failed attempt', async () => {
const io = makeIo();
const compute = {
query: vi.fn(),
validateSources: vi.fn(),
generateSources: vi.fn(),
};
const createPythonCompute = vi.fn(() => compute);
let attempt = 0;
const installRuntime = vi.fn(async (): Promise<ManagedPythonRuntimeInstallResult> => {
attempt += 1;
if (attempt === 1) {
throw new KtxExpectedError('uv missing');
}
return installResult();
});
const port = createLazyManagedPythonSemanticLayerComputePort({
cliVersion: '0.2.0',
installPolicy: 'auto',
io: io.io,
readStatus: vi.fn(async () => missingStatus()),
installRuntime,
createPythonCompute,
});
await expect(port.query({ sources: [], query: {} as never, dialect: 'postgres' })).rejects.toBeInstanceOf(
KtxExpectedError,
);
await port.query({ sources: [], query: {} as never, dialect: 'postgres' });
expect(installRuntime).toHaveBeenCalledTimes(2);
expect(compute.query).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,19 +1,61 @@
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { dirname, join } from 'node:path';
import { strToU8, zipSync } from 'fflate'; import { gzipSync, strToU8, zipSync } from 'fflate';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { KtxExpectedError } from '../src/errors.js';
import { import {
MISSING_UV_RUNTIME_INSTALL_MESSAGE,
doctorManagedPythonRuntime, doctorManagedPythonRuntime,
ensureManagedUv,
installManagedPythonRuntime, installManagedPythonRuntime,
managedPythonDaemonLayout, managedPythonDaemonLayout,
managedPythonRuntimeLayout, managedPythonRuntimeLayout,
managedUvPath,
readManagedPythonRuntimeStatus, readManagedPythonRuntimeStatus,
verifyRuntimeAsset, verifyRuntimeAsset,
type ManagedPythonRuntimeExec, type ManagedPythonRuntimeExec,
type ManagedUvRelease,
} from '../src/managed-python-runtime.js'; } from '../src/managed-python-runtime.js';
import type { ManagedUvPlatformKey } from '../src/managed-uv-release.js';
async function placeFakeUv(runtimeRoot: string): Promise<string> {
const uvPath = managedUvPath({ runtimeRoot });
await mkdir(dirname(uvPath), { recursive: true });
await writeFile(uvPath, '#!/bin/sh\n');
return uvPath;
}
function tarball(entries: Record<string, Uint8Array>): Uint8Array {
const blocks: Uint8Array[] = [];
for (const [name, data] of Object.entries(entries)) {
const header = new Uint8Array(512);
header.set(strToU8(name), 0);
header.set(strToU8('0000755\0'), 100);
header.set(strToU8(`${data.length.toString(8).padStart(11, '0')}\0`), 124);
blocks.push(header);
const padded = new Uint8Array(Math.ceil(data.length / 512) * 512);
padded.set(data);
blocks.push(padded);
}
blocks.push(new Uint8Array(1024));
const out = new Uint8Array(blocks.reduce((total, block) => total + block.length, 0));
let offset = 0;
for (const block of blocks) {
out.set(block, offset);
offset += block.length;
}
return out;
}
function releaseFor(file: string, contents: Uint8Array, key: ManagedUvPlatformKey): ManagedUvRelease {
return {
version: '9.9.9-test',
artifacts: {
[key]: { file, sha256: createHash('sha256').update(contents).digest('hex') },
},
};
}
function runtimeWheelContents(input: { label?: string; requiresPython?: string | null } = {}): Buffer { function runtimeWheelContents(input: { label?: string; requiresPython?: string | null } = {}): Buffer {
const label = input.label ?? 'runtime-wheel'; const label = input.label ?? 'runtime-wheel';
@ -246,10 +288,11 @@ describe('installManagedPythonRuntime', () => {
it('creates a venv, installs the core wheel, and writes a manifest', async () => { it('creates a venv, installs the core wheel, and writes a manifest', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const uvPath = await placeFakeUv(join(tempDir, 'runtime'));
const commands: Array<{ command: string; args: string[] }> = []; const commands: Array<{ command: string; args: string[] }> = [];
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => { const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => {
commands.push({ command, args }); commands.push({ command, args });
return { stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' }; return { stdout: command === uvPath && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' };
}); });
const result = await installManagedPythonRuntime({ const result = await installManagedPythonRuntime({
@ -262,11 +305,11 @@ describe('installManagedPythonRuntime', () => {
expect(result.status).toBe('installed'); expect(result.status).toBe('installed');
expect(commands).toEqual([ expect(commands).toEqual([
{ command: 'uv', args: ['--version'] }, { command: uvPath, args: ['--version'] },
{ command: 'uv', args: ['python', 'install', '3.13'] }, { command: uvPath, args: ['python', 'install', '3.13'] },
{ command: 'uv', args: ['venv', '--python', '3.13', result.layout.venvDir] }, { command: uvPath, args: ['venv', '--python', '3.13', result.layout.venvDir] },
{ {
command: 'uv', command: uvPath,
args: ['pip', 'install', '--python', result.layout.pythonPath, result.asset.wheelPath], args: ['pip', 'install', '--python', result.layout.pythonPath, result.asset.wheelPath],
}, },
]); ]);
@ -283,10 +326,11 @@ describe('installManagedPythonRuntime', () => {
it('disables repo uv config for managed runtime uv commands', async () => { it('disables repo uv config for managed runtime uv commands', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const uvPath = await placeFakeUv(join(tempDir, 'runtime'));
const commands: Array<{ command: string; args: string[]; env?: NodeJS.ProcessEnv }> = []; const commands: Array<{ command: string; args: string[]; env?: NodeJS.ProcessEnv }> = [];
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args, options) => { const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args, options) => {
commands.push({ command, args, env: options?.env }); commands.push({ command, args, env: options?.env });
return { stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.11.13\n' : '', stderr: '' }; return { stdout: command === uvPath && args[0] === '--version' ? 'uv 0.11.13\n' : '', stderr: '' };
}); });
await installManagedPythonRuntime({ await installManagedPythonRuntime({
@ -299,19 +343,20 @@ describe('installManagedPythonRuntime', () => {
}); });
expect(commands.map((call) => [call.command, call.args[0], call.env?.UV_NO_CONFIG, call.env?.PATH])).toEqual([ expect(commands.map((call) => [call.command, call.args[0], call.env?.UV_NO_CONFIG, call.env?.PATH])).toEqual([
['uv', '--version', '1', '/opt/homebrew/bin'], [uvPath, '--version', '1', '/opt/homebrew/bin'],
['uv', 'python', '1', '/opt/homebrew/bin'], [uvPath, 'python', '1', '/opt/homebrew/bin'],
['uv', 'venv', '1', '/opt/homebrew/bin'], [uvPath, 'venv', '1', '/opt/homebrew/bin'],
['uv', 'pip', '1', '/opt/homebrew/bin'], [uvPath, 'pip', '1', '/opt/homebrew/bin'],
]); ]);
}); });
it('installs the local-embeddings extra when requested', async () => { it('installs the local-embeddings extra when requested', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'embedding-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'embedding-wheel' });
const uvPath = await placeFakeUv(join(tempDir, 'runtime'));
const commands: Array<{ command: string; args: string[] }> = []; const commands: Array<{ command: string; args: string[] }> = [];
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => { const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => {
commands.push({ command, args }); commands.push({ command, args });
return { stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' }; return { stdout: command === uvPath && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' };
}); });
const result = await installManagedPythonRuntime({ const result = await installManagedPythonRuntime({
@ -323,38 +368,72 @@ describe('installManagedPythonRuntime', () => {
}); });
expect(commands.at(-1)).toEqual({ expect(commands.at(-1)).toEqual({
command: 'uv', command: uvPath,
args: ['pip', 'install', '--python', result.layout.pythonPath, `${result.asset.wheelPath}[local-embeddings]`], args: ['pip', 'install', '--python', result.layout.pythonPath, `${result.asset.wheelPath}[local-embeddings]`],
}); });
const manifest = JSON.parse(await readFile(result.layout.manifestPath, 'utf8')) as { features: string[] }; const manifest = JSON.parse(await readFile(result.layout.manifestPath, 'utf8')) as { features: string[] };
expect(manifest.features).toEqual(['core', 'local-embeddings']); expect(manifest.features).toEqual(['core', 'local-embeddings']);
}); });
it('fails with the hard-prerequisite message when uv is missing', async () => { it('attempts the pinned uv download from github.com and rejects checksum mismatches', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const commands: Array<{ command: string; args: string[] }> = []; const runtimeRoot = join(tempDir, 'runtime');
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => { const archive = gzipSync(tarball({ 'uv-test/uv': strToU8('#!/bin/sh\necho uv\n') }));
commands.push({ command, args }); const fetchUvArtifact = vi.fn(async () => archive);
throw new Error('spawn uv ENOENT'); const exec: ManagedPythonRuntimeExec = vi.fn(async () => ({ stdout: 'uv 9.9.9\n', stderr: '' }));
});
await expect( const error = await installManagedPythonRuntime({
installManagedPythonRuntime({
cliVersion: '0.2.0', cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'), runtimeRoot,
assetDir, assetDir,
features: ['core'], features: ['core'],
exec, exec,
}), fetchUvArtifact,
).rejects.toThrow(MISSING_UV_RUNTIME_INSTALL_MESSAGE); }).catch((caught: unknown) => caught);
expect(commands).toEqual([{ command: 'uv', args: ['--version'] }]); expect(fetchUvArtifact).toHaveBeenCalledTimes(1);
expect(fetchUvArtifact).toHaveBeenCalledWith(
expect.stringMatching(/^https:\/\/github\.com\/astral-sh\/uv\/releases\/download\//),
);
expect(error).toBeInstanceOf(KtxExpectedError);
expect((error as Error).message).toContain('failed checksum verification');
expect(exec).not.toHaveBeenCalled();
});
it('fails with download guidance and preserves the existing runtime when uv cannot be fetched', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const runtimeRoot = join(tempDir, 'runtime');
const exec: ManagedPythonRuntimeExec = vi.fn(async () => ({ stdout: '', stderr: '' }));
const fetchUvArtifact = vi.fn(async () => {
throw new Error('getaddrinfo ENOTFOUND github.com');
});
const survivingRuntimeFile = join(runtimeRoot, '0.2.0', 'install.log');
await mkdir(dirname(survivingRuntimeFile), { recursive: true });
await writeFile(survivingRuntimeFile, 'stale runtime contents\n');
const error = await installManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot,
assetDir,
features: ['core'],
exec,
fetchUvArtifact,
}).catch((caught: unknown) => caught);
// KtxExpectedError keeps this user-environment outcome out of Error Tracking.
expect(error).toBeInstanceOf(KtxExpectedError);
expect((error as Error).message).toContain('could not download uv');
expect((error as Error).message).toContain('ktx admin runtime install --yes');
expect(exec).not.toHaveBeenCalled();
// A failed uv acquisition must not wipe whatever runtime is already on disk.
await expect(readFile(survivingRuntimeFile, 'utf8')).resolves.toContain('stale');
}); });
it('reuses an existing compatible runtime when force is false', async () => { it('reuses an existing compatible runtime when force is false', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const uvPath = await placeFakeUv(join(tempDir, 'runtime'));
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({ const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stdout: command === uvPath && args[0] === '--version' ? 'uv 0.9.5\n' : '',
stderr: '', stderr: '',
})); }));
@ -383,14 +462,15 @@ describe('installManagedPythonRuntime', () => {
it('keeps failed install logs in the versioned runtime directory', async () => { it('keeps failed install logs in the versioned runtime directory', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const uvPath = await placeFakeUv(join(tempDir, 'runtime'));
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => { const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => {
if (command === 'uv' && args[0] === 'venv') { if (command === uvPath && args[0] === 'venv') {
throw Object.assign(new Error('uv venv failed'), { throw Object.assign(new Error('uv venv failed'), {
stdout: 'creating\n', stdout: 'creating\n',
stderr: '× No solution found\n╰─▶ current Python version (3.12.3) does not satisfy Python>=3.13\n', stderr: '× No solution found\n╰─▶ current Python version (3.12.3) does not satisfy Python>=3.13\n',
}); });
} }
return { stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' }; return { stdout: command === uvPath && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' };
}); });
await expect( await expect(
@ -404,11 +484,98 @@ describe('installManagedPythonRuntime', () => {
).rejects.toThrow(/current Python version \(3\.12\.3\) does not satisfy Python>=3\.13/); ).rejects.toThrow(/current Python version \(3\.12\.3\) does not satisfy Python>=3\.13/);
const log = await readFile(join(tempDir, 'runtime', '0.2.0', 'install.log'), 'utf8'); const log = await readFile(join(tempDir, 'runtime', '0.2.0', 'install.log'), 'utf8');
expect(log).toContain('$ uv venv --python 3.13'); expect(log).toContain(`$ ${uvPath} venv --python 3.13`);
expect(log).toContain('current Python version (3.12.3) does not satisfy Python>=3.13'); expect(log).toContain('current Python version (3.12.3) does not satisfy Python>=3.13');
}); });
}); });
describe('ensureManagedUv', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-managed-uv-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it('downloads, verifies, and extracts uv from a tar.gz artifact, then reuses the cached binary', async () => {
const binary = strToU8('#!/bin/sh\necho uv\n');
const archive = gzipSync(tarball({ 'uv-test/': new Uint8Array(0), 'uv-test/uvx': strToU8('x'), 'uv-test/uv': binary }));
const release = releaseFor('uv-test.tar.gz', archive, 'linux-x64');
const fetchArtifact = vi.fn(async () => archive);
const uvPath = await ensureManagedUv({
platform: 'linux',
arch: 'x64',
runtimeRoot: join(tempDir, 'runtime'),
fetchArtifact,
release,
});
expect(uvPath).toBe(join(tempDir, 'runtime', 'uv', '9.9.9-test', 'uv'));
await expect(readFile(uvPath, 'utf8')).resolves.toBe('#!/bin/sh\necho uv\n');
const again = await ensureManagedUv({
platform: 'linux',
arch: 'x64',
runtimeRoot: join(tempDir, 'runtime'),
fetchArtifact,
release,
});
expect(again).toBe(uvPath);
expect(fetchArtifact).toHaveBeenCalledTimes(1);
});
it('extracts uv.exe from a zip artifact on Windows', async () => {
const archive = zipSync({ 'uv.exe': strToU8('MZ-uv'), 'uvx.exe': strToU8('MZ-uvx') });
const release = releaseFor('uv-test.zip', archive, 'win32-x64');
const uvPath = await ensureManagedUv({
platform: 'win32',
arch: 'x64',
runtimeRoot: join(tempDir, 'runtime'),
fetchArtifact: vi.fn(async () => archive),
release,
});
expect(uvPath).toBe(join(tempDir, 'runtime', 'uv', '9.9.9-test', 'uv.exe'));
await expect(readFile(uvPath, 'utf8')).resolves.toBe('MZ-uv');
});
it('rejects an artifact whose checksum does not match the pin', async () => {
const archive = gzipSync(tarball({ 'uv-test/uv': strToU8('uv') }));
const release = releaseFor('uv-test.tar.gz', archive, 'linux-x64');
release.artifacts['linux-x64']!.sha256 = 'b'.repeat(64);
const error = await ensureManagedUv({
platform: 'linux',
arch: 'x64',
runtimeRoot: join(tempDir, 'runtime'),
fetchArtifact: vi.fn(async () => archive),
release,
}).catch((caught: unknown) => caught);
expect(error).toBeInstanceOf(KtxExpectedError);
expect((error as Error).message).toContain('failed checksum verification');
});
it('fails with manual-placement guidance on platforms without a pinned artifact', async () => {
const error = await ensureManagedUv({
platform: 'sunos',
arch: 'x64',
runtimeRoot: join(tempDir, 'runtime'),
fetchArtifact: vi.fn(),
release: { version: '9.9.9-test', artifacts: {} },
}).catch((caught: unknown) => caught);
expect(error).toBeInstanceOf(KtxExpectedError);
expect((error as Error).message).toContain('does not bundle uv for sunos-x64');
expect((error as Error).message).toContain(join(tempDir, 'runtime', 'uv', '9.9.9-test', 'uv'));
});
});
describe('readManagedPythonRuntimeStatus', () => { describe('readManagedPythonRuntimeStatus', () => {
let tempDir: string; let tempDir: string;
@ -433,8 +600,9 @@ describe('readManagedPythonRuntimeStatus', () => {
it('reports ready when manifest and executables exist', async () => { it('reports ready when manifest and executables exist', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const uvPath = await placeFakeUv(join(tempDir, 'runtime'));
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({ const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stdout: command === uvPath && args[0] === '--version' ? 'uv 0.9.5\n' : '',
stderr: '', stderr: '',
})); }));
const install = await installManagedPythonRuntime({ const install = await installManagedPythonRuntime({
@ -460,8 +628,9 @@ describe('readManagedPythonRuntimeStatus', () => {
it('reports broken when an executable is missing', async () => { it('reports broken when an executable is missing', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const uvPath = await placeFakeUv(join(tempDir, 'runtime'));
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({ const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stdout: command === uvPath && args[0] === '--version' ? 'uv 0.9.5\n' : '',
stderr: '', stderr: '',
})); }));
await installManagedPythonRuntime({ await installManagedPythonRuntime({
@ -496,8 +665,9 @@ describe('doctorManagedPythonRuntime', () => {
it('checks uv, bundled assets, and installed runtime status', async () => { it('checks uv, bundled assets, and installed runtime status', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const uvPath = await placeFakeUv(join(tempDir, 'runtime'));
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({ const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stdout: command === uvPath && args[0] === '--version' ? 'uv 0.9.5\n' : '',
stderr: '', stderr: '',
})); }));
@ -513,28 +683,27 @@ describe('doctorManagedPythonRuntime', () => {
['asset', 'pass'], ['asset', 'pass'],
['runtime', 'fail'], ['runtime', 'fail'],
]); ]);
expect(checks[0]?.detail).toBe(`uv 0.9.5 (managed: ${uvPath})`);
expect(checks[2]?.fix).toBe('Run: ktx admin runtime install --yes'); expect(checks[2]?.fix).toBe('Run: ktx admin runtime install --yes');
}); });
it('reports uv as a hard prerequisite when uv is missing', async () => { it('fails the uv check with download guidance when uv cannot be acquired', async () => {
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' }); const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
const exec: ManagedPythonRuntimeExec = vi.fn(async () => { const exec: ManagedPythonRuntimeExec = vi.fn(async () => ({ stdout: '', stderr: '' }));
throw new Error('spawn uv ENOENT');
});
const checks = await doctorManagedPythonRuntime({ const checks = await doctorManagedPythonRuntime({
cliVersion: '0.2.0', cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'), runtimeRoot: join(tempDir, 'runtime'),
assetDir, assetDir,
exec, exec,
fetchUvArtifact: vi.fn(async () => {
throw new Error('getaddrinfo ENOTFOUND github.com');
}),
}); });
expect(checks[0]).toEqual({ expect(checks[0]?.id).toBe('uv');
id: 'uv', expect(checks[0]?.status).toBe('fail');
label: 'uv', expect(checks[0]?.detail).toContain('could not download uv');
status: 'fail', expect(checks[0]?.fix).toBe('Check network access to github.com and run: ktx admin runtime install --yes');
detail: MISSING_UV_RUNTIME_INSTALL_MESSAGE,
fix: 'Install uv, make sure it is on PATH, and run: ktx admin runtime install --yes',
});
}); });
}); });

View file

@ -4,6 +4,7 @@ import { createLocalProjectMcpContextPorts } from '../src/context/mcp/local-proj
import { createLocalProjectMemoryIngest } from '../src/context/memory/local-memory.js'; import { createLocalProjectMemoryIngest } from '../src/context/memory/local-memory.js';
import { resolveProjectEmbeddingProvider } from '../src/embedding-resolution.js'; import { resolveProjectEmbeddingProvider } from '../src/embedding-resolution.js';
import { createKtxCliScanConnector } from '../src/local-scan-connectors.js'; import { createKtxCliScanConnector } from '../src/local-scan-connectors.js';
import { createLazyManagedPythonSemanticLayerComputePort } from '../src/managed-python-command.js';
import { createKtxMcpServerFactory } from '../src/mcp-server-factory.js'; import { createKtxMcpServerFactory } from '../src/mcp-server-factory.js';
type FakeEmbeddingProvider = { type FakeEmbeddingProvider = {
@ -62,7 +63,7 @@ vi.mock('../src/local-scan-connectors.js', () => ({
})); }));
vi.mock('../src/managed-python-command.js', () => ({ vi.mock('../src/managed-python-command.js', () => ({
createManagedPythonSemanticLayerComputePort: vi.fn(async () => mocks.semanticLayerCompute), createLazyManagedPythonSemanticLayerComputePort: vi.fn(() => mocks.semanticLayerCompute),
})); }));
vi.mock('../src/managed-python-http.js', () => ({ vi.mock('../src/managed-python-http.js', () => ({
@ -124,6 +125,13 @@ describe('createKtxMcpServerFactory', () => {
expect(provider.embed).toHaveBeenCalledWith('gross revenue'); expect(provider.embed).toHaveBeenCalledWith('gross revenue');
expect(provider.embedMany).toHaveBeenCalledWith(['gross revenue']); expect(provider.embedMany).toHaveBeenCalledWith(['gross revenue']);
expect(createKtxCliScanConnector).toHaveBeenCalledWith(project, 'warehouse'); expect(createKtxCliScanConnector).toHaveBeenCalledWith(project, 'warehouse');
// The server must wire the lazy compute port so startup never blocks on (or
// fails over) a missing managed Python runtime / uv.
expect(createLazyManagedPythonSemanticLayerComputePort).toHaveBeenCalledWith({
cliVersion: '0.5.0',
installPolicy: 'auto',
io,
});
expect(contextOptions).toMatchObject({ expect(contextOptions).toMatchObject({
queryExecutor: mocks.queryExecutor, queryExecutor: mocks.queryExecutor,
semanticLayerCompute: mocks.semanticLayerCompute, semanticLayerCompute: mocks.semanticLayerCompute,

View file

@ -0,0 +1,95 @@
#!/usr/bin/env node
import { writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const manifestModulePath = path.join(scriptDir, '..', 'packages', 'cli', 'src', 'managed-uv-release.ts');
// Linux always uses the musl-static build: it runs on both glibc and musl
// distributions, so the CLI never has to detect libc at runtime.
const PLATFORM_ARTIFACTS = {
'darwin-arm64': 'uv-aarch64-apple-darwin.tar.gz',
'darwin-x64': 'uv-x86_64-apple-darwin.tar.gz',
'linux-arm64': 'uv-aarch64-unknown-linux-musl.tar.gz',
'linux-x64': 'uv-x86_64-unknown-linux-musl.tar.gz',
'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip',
'win32-x64': 'uv-x86_64-pc-windows-msvc.zip',
};
function parseSha256File(contents, file) {
const hash = contents.trim().split(/\s+/)[0]?.replace(/^\*/, '');
if (!/^[a-f0-9]{64}$/.test(hash ?? '')) {
throw new Error(`Unexpected sha256 file contents for ${file}: ${contents.slice(0, 120)}`);
}
return hash;
}
export function renderUvReleaseModule(version, artifacts) {
const keys = Object.keys(PLATFORM_ARTIFACTS);
for (const key of keys) {
if (!artifacts[key]?.file || !artifacts[key]?.sha256) {
throw new Error(`Missing artifact entry for ${key}`);
}
}
const entries = keys
.map(
(key) =>
` '${key}': { file: '${artifacts[key].file}', sha256: '${artifacts[key].sha256}' }, // pragma: allowlist secret`,
)
.join('\n');
return `// Generated by scripts/refresh-uv-manifest.mjs. Do not edit by hand.
// Regenerate with: node scripts/refresh-uv-manifest.mjs [<uv-version>]
export type ManagedUvPlatformKey =
${keys.map((key) => ` | '${key}'`).join('\n')};
export interface ManagedUvArtifact {
file: string;
sha256: string;
}
export const MANAGED_UV_VERSION = '${version}';
export const MANAGED_UV_ARTIFACTS: Record<ManagedUvPlatformKey, ManagedUvArtifact> = {
${entries}
};
`;
}
export async function refreshUvManifest(options = {}) {
const fetchImpl = options.fetch ?? fetch;
const writeFile = options.writeFile ?? writeFileSync;
const log = options.log ?? console.log;
const outputPath = options.outputPath ?? manifestModulePath;
let version = options.version;
if (!version) {
const response = await fetchImpl('https://api.github.com/repos/astral-sh/uv/releases/latest');
if (!response.ok) {
throw new Error(`Failed to resolve latest uv release: HTTP ${response.status}`);
}
version = (await response.json()).tag_name;
}
const artifacts = {};
for (const [key, file] of Object.entries(PLATFORM_ARTIFACTS)) {
const url = `https://github.com/astral-sh/uv/releases/download/${version}/${file}.sha256`;
const response = await fetchImpl(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
}
artifacts[key] = { file, sha256: parseSha256File(await response.text(), file) };
}
writeFile(outputPath, renderUvReleaseModule(version, artifacts));
log(`Pinned uv ${version} into ${outputPath}`);
return { version, artifacts };
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
refreshUvManifest({ version: process.argv[2] }).catch((error) => {
console.error(error);
process.exit(1);
});
}

View file

@ -0,0 +1,69 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { refreshUvManifest, renderUvReleaseModule } from './refresh-uv-manifest.mjs';
const ALL_KEYS = ['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', 'win32-arm64', 'win32-x64'];
function artifactsFixture() {
return Object.fromEntries(
ALL_KEYS.map((key, index) => [key, { file: `uv-${key}.tar.gz`, sha256: String(index).repeat(64).slice(0, 64) }]),
);
}
describe('renderUvReleaseModule', () => {
it('renders a typed module with every platform key and the pinned version', () => {
const module = renderUvReleaseModule('1.2.3', artifactsFixture());
assert.match(module, /MANAGED_UV_VERSION = '1\.2\.3'/);
assert.match(module, /Generated by scripts\/refresh-uv-manifest\.mjs/);
for (const key of ALL_KEYS) {
assert.match(module, new RegExp(`'${key}': \\{ file: 'uv-${key}\\.tar\\.gz', sha256: '[a-f0-9]{64}' \\}`));
}
});
it('rejects an incomplete platform map', () => {
const artifacts = artifactsFixture();
delete artifacts['win32-arm64'];
assert.throws(() => renderUvReleaseModule('1.2.3', artifacts), /Missing artifact entry for win32-arm64/);
});
});
describe('refreshUvManifest', () => {
it('fetches per-artifact sha256 files and writes the module', async () => {
const written = [];
const fetched = [];
const fetchStub = async (url) => {
fetched.push(url);
return {
ok: true,
text: async () => `${'a'.repeat(64)} *${url.split('/').at(-1).replace('.sha256', '')}\n`,
};
};
const result = await refreshUvManifest({
version: '1.2.3',
fetch: fetchStub,
writeFile: (path, contents) => written.push({ path, contents }),
log: () => {},
outputPath: '/tmp/managed-uv-release.ts',
});
assert.equal(result.version, '1.2.3');
assert.equal(fetched.length, ALL_KEYS.length);
assert.ok(fetched.every((url) => url.endsWith('.sha256') && url.includes('/download/1.2.3/')));
assert.equal(written.length, 1);
assert.match(written[0].contents, /MANAGED_UV_VERSION = '1\.2\.3'/);
});
it('rejects malformed sha256 file contents', async () => {
await assert.rejects(
refreshUvManifest({
version: '1.2.3',
fetch: async () => ({ ok: true, text: async () => '<html>proxy login</html>' }),
writeFile: () => {},
log: () => {},
}),
/Unexpected sha256 file contents/,
);
});
});