fix(connections): enforce scan_enabled:false on explicit scan/ingest commands

scan_enabled:false promised the connection is 'never used as a scan/ingest
target,' but the predicate only gated automatic selection — explicit
ktx scan <id> / ktx ingest <id> still resolved the connection id and reached the
live-database introspection path, so an execute-only connection could still be
scanned or ingested.

Guard runKtxScan and runKtxIngest at entry: if the target connection is
execute-only, refuse with an actionable error (remove the flag to scan, or use
ktx sql to query) before doing any work. This makes the flag a single declaration
honored on every scan/ingest entry point, not just auto-selection.
This commit is contained in:
Andrey Avtomonov 2026-06-09 14:28:05 +02:00
parent f446d207ba
commit 9ac37166f5
5 changed files with 85 additions and 4 deletions

View file

@ -8,6 +8,7 @@ import type { MemoryFlowEvent, MemoryFlowReplayInput } from './context/ingest/me
import { renderMemoryFlowReplay } from './context/ingest/memory-flow/render.js';
import type { KtxSqlQueryExecutorPort } from './context/connections/query-executor.js';
import { loadKtxProject, type KtxLocalProject } from './context/project/project.js';
import { isExecuteOnlyConnection } from './context/connections/local-warehouse-descriptor.js';
import { getKtxCliPackageInfo } from './cli-runtime.js';
import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js';
@ -695,6 +696,13 @@ export async function runKtxIngest(
const project = await loadKtxProject({ projectDir: args.projectDir });
const env = deps.env ?? process.env;
if (args.command === 'run') {
if (isExecuteOnlyConnection(project.config.connections[args.connectionId])) {
io.stderr.write(
`Connection '${args.connectionId}' is registered for SQL execution only (scan_enabled: false) and ` +
'cannot be ingested. Remove scan_enabled: false to make it a scan/ingest target, or use `ktx sql` to query it.\n',
);
return 1;
}
const resolveEmbeddingProvider = deps.resolveEmbeddingProvider ?? resolveProjectEmbeddingProvider;
const resolution = await resolveEmbeddingProvider(project, {
mode: 'ensure',

View file

@ -1,6 +1,7 @@
import type { KtxProgressPort, KtxScanMode, KtxScanReport, KtxScanWarning } from './context/scan/types.js';
import { runLocalScan } from './context/scan/local-scan.js';
import { loadKtxProject, type KtxLocalProject } from './context/project/project.js';
import { isExecuteOnlyConnection } from './context/connections/local-warehouse-descriptor.js';
import { getKtxCliPackageInfo } from './cli-runtime.js';
import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
import type { KtxCliIo } from './index.js';
@ -326,6 +327,13 @@ export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps
let project: KtxLocalProject | undefined;
try {
project = await loadKtxProject({ projectDir: args.projectDir });
if (isExecuteOnlyConnection(project.config.connections[args.connectionId])) {
io.stderr.write(
`Connection '${args.connectionId}' is registered for SQL execution only (scan_enabled: false) and ` +
'cannot be scanned. Remove scan_enabled: false to make it a scan target, or use `ktx sql` to query it.\n',
);
return 1;
}
const resolveEmbeddingProvider = deps.resolveEmbeddingProvider ?? resolveProjectEmbeddingProvider;
const resolution = await resolveEmbeddingProvider(project, {
mode: 'ensure',