fix(setup): unblock clean Linux installs and add enabled_tables allowlist

- Pin managed Python runtime to 3.13 via `uv venv --python 3.13` so installs
  don't pick the system 3.12 on Ubuntu 24.04 and fail at wheel install.
- Sanitize NO_PROXY/no_proxy for the daemon child process — drop IPv6 CIDR
  entries that httpx rejects with InvalidURL (OrbStack injects these by
  default).
- Add `enabled_tables` allowlist on warehouse connections (zod schema +
  live-database introspection filter) to scope ingest to specific tables.
- Add `getting-started/troubleshooting-linux` docs page covering the Python
  3.13 prerequisite, IPv6 proxy gotcha, and a minimal working recipe; link
  it from the quickstart troubleshooting table and the llms-docs map.
- Make docs-site origin overridable via `KTX_DOCS_ORIGIN` so local builds
  can serve under host.docker.internal.
This commit is contained in:
Andrey Avtomonov 2026-05-19 14:17:52 +02:00
parent c513d61dca
commit 2403f58eff
12 changed files with 307 additions and 8 deletions

View file

@ -4,6 +4,7 @@ import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
readManagedPythonDaemonStatus,
sanitizeProxyEnv,
startManagedPythonDaemon,
stopAllManagedPythonDaemons,
stopManagedPythonDaemon,
@ -404,3 +405,38 @@ describe('managed Python daemon lifecycle', () => {
expect(await readFile(layout(tempDir).daemonStatePath, 'utf8')).toContain('"pid": 4242');
});
});
describe('sanitizeProxyEnv', () => {
it('removes IPv6 CIDR entries from NO_PROXY that crash httpx', () => {
const cleaned = sanitizeProxyEnv({
NO_PROXY: 'localhost,127.0.0.1,fd07:b51a:cc66:f0::/64,*.orb.internal',
no_proxy: 'localhost,127.0.0.1,fd07:b51a:cc66:f0::/64,*.orb.internal',
});
expect(cleaned.NO_PROXY).toBe('localhost,127.0.0.1,*.orb.internal');
expect(cleaned.no_proxy).toBe('localhost,127.0.0.1,*.orb.internal');
});
it('deletes NO_PROXY entirely when every entry is unparseable', () => {
const cleaned = sanitizeProxyEnv({ NO_PROXY: 'fd07::/64,::1' });
expect(cleaned.NO_PROXY).toBeUndefined();
});
it('preserves IPv4 addresses, IPv4 CIDRs, hostnames, and wildcards', () => {
const cleaned = sanitizeProxyEnv({
NO_PROXY: '127.0.0.0/8,10.0.0.1,localhost,*.example.com',
});
expect(cleaned.NO_PROXY).toBe('127.0.0.0/8,10.0.0.1,localhost,*.example.com');
});
it('leaves other env vars untouched', () => {
const cleaned = sanitizeProxyEnv({ PATH: '/usr/bin', NO_PROXY: '::1', FOO: 'bar' });
expect(cleaned.PATH).toBe('/usr/bin');
expect(cleaned.FOO).toBe('bar');
expect(cleaned.NO_PROXY).toBeUndefined();
});
it('does nothing when NO_PROXY is not set', () => {
const cleaned = sanitizeProxyEnv({ PATH: '/usr/bin' });
expect(cleaned).toEqual({ PATH: '/usr/bin' });
});
});

View file

@ -697,7 +697,7 @@ export async function startManagedPythonDaemon(
detached: true,
stdio: ['ignore', stdout.fd, stderr.fd],
env: {
...process.env,
...sanitizeProxyEnv(process.env),
KTX_DAEMON_VERSION: options.cliVersion,
},
},
@ -807,3 +807,32 @@ export async function stopAllManagedPythonDaemons(
scanErrors: discovery.scanErrors,
};
}
/**
* Filter NO_PROXY/no_proxy values to remove entries httpx cannot parse.
*
* httpx (used by the Python daemon via huggingface_hub / sentence-transformers)
* treats each comma-separated NO_PROXY entry as a URL pattern. Raw IPv6 CIDR
* blocks like `fd07:b51a:cc66:f0::/64` raise `InvalidURL` and crash the daemon.
* OrbStack and similar Docker setups inject such entries by default.
*
* We drop any entry containing `::` (the unambiguous IPv6 marker) but keep
* IPv4 addresses, IPv4 CIDRs, hostnames, and wildcard hosts intact.
*/
export function sanitizeProxyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const result: NodeJS.ProcessEnv = { ...env };
for (const key of ['NO_PROXY', 'no_proxy']) {
const value = result[key];
if (typeof value !== 'string' || value.length === 0) continue;
const kept = value
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0 && !entry.includes('::'));
if (kept.length === 0) {
delete result[key];
} else {
result[key] = kept.join(',');
}
}
return result;
}

View file

@ -222,7 +222,7 @@ describe('installManagedPythonRuntime', () => {
expect(result.status).toBe('installed');
expect(commands).toEqual([
{ command: 'uv', args: ['--version'] },
{ command: 'uv', args: ['venv', result.layout.venvDir] },
{ command: 'uv', args: ['venv', result.layout.venvDir, '--python', '3.13'] },
{
command: 'uv',
args: ['pip', 'install', '--python', result.layout.pythonPath, result.asset.wheelPath],

View file

@ -12,6 +12,16 @@ const execFileAsync = promisify(execFile);
export const runtimeFeatureSchema = z.enum(['core', 'local-embeddings']);
export type KtxRuntimeFeature = z.infer<typeof runtimeFeatureSchema>;
/**
* Python version the managed runtime venv must be built with. KTX's bundled
* wheels declare `requires-python = ">=3.13"`; without an explicit `--python`
* flag, `uv venv` may pick a too-old system Python (Ubuntu 24.04 ships 3.12)
* and the subsequent `uv pip install` fails late with a confusing "package
* requires Python >=3.13" error. Pinning here pushes uv to auto-download the
* right interpreter via its python-management feature.
*/
export const MANAGED_RUNTIME_PYTHON_VERSION = '3.13';
const runtimeAssetManifestSchema = z.object({
schemaVersion: z.literal(1),
distributionName: z.literal('kaelio-ktx'),
@ -334,7 +344,7 @@ export async function installManagedPythonRuntime(
exec,
logPath: layout.installLogPath,
command: 'uv',
args: ['venv', layout.venvDir],
args: ['venv', layout.venvDir, '--python', MANAGED_RUNTIME_PYTHON_VERSION],
env: uvEnv,
});
const wheelSpec = features.includes('local-embeddings') ? `${asset.wheelPath}[local-embeddings]` : asset.wheelPath;