Files
OmniRoute/src/lib/copilot/codegraphKnowledge.ts
Armin Anton” ∴ 10276821cd Integration: security tier + self-hosted operator blockers (rebased onto v3.8.51) (#10952)
Validated on the resolved merge against the current tip (527da656 + the post-#11281 rebaseline): the single conflict was a comment-only collision in providers/[id]/models/route.ts (kept the tip's #10828-ordering note). Focused suites 125/125 across all 13 touched test files (build-sqlite-stub, cc-compatible, copilot-claude-messages, copilot-gemini-route, executor-github, ghe-copilot, github-copilot-discovery-token, github-copilot-model-discovery, noauth-sibling-7620, provider-header-profiles, provider-models-config, request-log-payloads, upstream-error-passthrough), typecheck:core clean, file-size/changelog-integrity OK. Merged --admin over the inherited 2026-08-23 base-red cluster (#9985) — the reds are proven tip failures (CLI catalog cluster + @testing-library allowlist, being drained by #11280), not from this diff. Note: the rebase means several items the body listed (relay x-relay-path SSRF, /v1/search blocked-providers, #10736 rotation fence, #10903, #10865, #10899, #10916) already landed upstream and are NOT in this delta — the delta is: better-sqlite3 build guard + build heap/worker caps + telemetry-off (#10060 re-derived), credential-echo passthrough refusal + OCR/moderation redaction + call-log key redaction, Copilot CLI 1.0.81-6 wire identity + Claude→/v1/messages name-matched routing + discovery token fix, CC model_not_found 400, compat overrides for no-auth aliases (#7620-pinned). The Copilot wire-identity change is the one to watch in production. Thank you @arminanton — and the ported-author credits in the commit history (@rqzbeh, yidecode, the #10899/#10916 authors) are preserved. Your config-posture finding (REQUIRE_API_KEY default vs 0.0.0.0) is noted for a maintainer decision, as you scoped it.
2026-08-23 16:51:25 -03:00

246 lines
7.3 KiB
TypeScript

/**
* Copilot CodeGraph Knowledge Module
*
* Provides the Copilot with read-only access to the project's CodeGraph index.
* Queries the `.codegraph/codegraph.db` SQLite database to find symbols,
* explore relationships, list files, and search documentation.
*
* Falls back gracefully if the CodeGraph DB does not exist (e.g., production installs).
*/
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { isNextBuildPhase } from "../buildPhase";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface CodeGraphNode {
id: string;
kind: string;
name: string;
qualifiedName: string;
filePath: string;
language: string;
startLine: number;
endLine: number;
signature?: string;
docstring?: string;
isExported: boolean;
visibility?: string;
}
// ---------------------------------------------------------------------------
// Database access (lazy loaded)
// ---------------------------------------------------------------------------
let _db: unknown = null;
let dbPathOverride: string | null | undefined;
/** Override the index path for deterministic tests and embedded callers. */
export function setCodeGraphPathForTest(path: string | null | undefined): void {
dbPathOverride = path;
_db = null;
}
function getDbPath(): string | null {
if (dbPathOverride !== undefined) return dbPathOverride;
// Try project root first (dev), then cwd, then DATA_DIR
const candidates = [
join(process.cwd(), ".codegraph", "codegraph.db"),
join(process.cwd(), "..", ".codegraph", "codegraph.db"),
];
// Try to resolve from the project root
const __dirname = dirname(fileURLToPath(import.meta.url));
// Walk up to find .codegraph/
let dir = __dirname;
for (let i = 0; i < 10; i++) {
const candidate = join(dir, ".codegraph", "codegraph.db");
if (existsSync(candidate)) return candidate;
const parent = join(dir, "..");
if (parent === dir) break;
dir = parent;
}
for (const c of candidates) {
if (existsSync(c)) return c;
}
return null;
}
export interface CodeGraphQueryResult {
success: boolean;
data: unknown;
error?: string;
engine: "sqlite" | "cli" | "none";
}
function queryDb(query: string, params: unknown[] = []): CodeGraphQueryResult {
try {
if (!_db) {
const dbPath = getDbPath();
if (!dbPath) {
return { success: false, data: null, error: "CodeGraph DB not found", engine: "none" };
}
// Dynamic import to avoid hard dependency on better-sqlite3
_db = null;
// Use better-sqlite3 if available
try {
// Never load the native better-sqlite3 addon during the Next.js build:
// its Statement destructor aborts with SIGABRT at build-worker teardown
// (node::RemoveEnvironmentCleanupHook). This path is not exercised during
// build, so failing closed to "not available" is safe. (#10060)
if (isNextBuildPhase()) throw new Error("Skip better-sqlite3 during build");
const Database = require("better-sqlite3");
_db = new Database(dbPath, { readonly: true });
} catch {
return {
success: false,
data: null,
error: "better-sqlite3 not available",
engine: "none",
};
}
}
const stmt = (
_db as { prepare: (sql: string) => { all: (params: unknown[]) => unknown[] } }
).prepare(query);
const rows = stmt.all(params);
return { success: true, data: rows, engine: "sqlite" };
} catch (err) {
return {
success: false,
data: null,
error: err instanceof Error ? err.message : "Unknown error",
engine: "none",
};
}
}
// ---------------------------------------------------------------------------
// Search operations
// ---------------------------------------------------------------------------
/**
* Search symbols by name (exact or partial match via FTS).
*/
export function searchSymbols(query: string, limit = 20): CodeGraphQueryResult {
const sql = `
SELECT n.*
FROM nodes n
JOIN nodes_fts fts ON n.id = fts.id
WHERE nodes_fts MATCH ?
ORDER BY rank
LIMIT ?
`;
// Escape FTS special chars and create prefix query
const sanitized = query.replace(/[^a-zA-Z0-9_]/g, " ").trim();
if (!sanitized) {
// Fallback to LIKE if query is empty after sanitization
return queryDb(`SELECT * FROM nodes WHERE lower(name) LIKE ? ORDER BY kind, name LIMIT ?`, [
`%${query.toLowerCase()}%`,
limit,
]);
}
const ftsQuery = sanitized
.split(/\s+/)
.map((w) => `"${w}"*`)
.join(" AND ");
return queryDb(sql, [ftsQuery, limit]);
}
/**
* Find callers of a symbol (edges where target matches).
*/
export function findCallers(symbolName: string, limit = 20): CodeGraphQueryResult {
return queryDb(
`SELECT e.id as edgeId, e.kind as edgeKind, e.line, e.col,
s.id as sourceId, s.name as sourceName, s.kind as sourceKind,
s.file_path as sourceFile, s.start_line as sourceLine,
t.name as targetName, t.file_path as targetFile
FROM edges e
JOIN nodes s ON e.source = s.id
JOIN nodes t ON e.target = t.id
WHERE t.name = ?
ORDER BY e.kind
LIMIT ?`,
[symbolName, limit]
);
}
/**
* Find callees of a symbol (edges where source matches).
*/
export function findCallees(symbolName: string, limit = 20): CodeGraphQueryResult {
return queryDb(
`SELECT e.id as edgeId, e.kind as edgeKind, e.line, e.col,
s.name as sourceName,
t.id as targetId, t.name as targetName, t.kind as targetKind,
t.file_path as targetFile, t.start_line as targetLine
FROM edges e
JOIN nodes s ON e.source = s.id
JOIN nodes t ON e.target = t.id
WHERE s.name = ?
ORDER BY e.kind
LIMIT ?`,
[symbolName, limit]
);
}
/**
* Get context for a file: all symbols defined in it.
*/
export function getFileContext(filePath: string): CodeGraphQueryResult {
// Try matching on suffix of file_path (many nodes store paths relative to root)
return queryDb(
`SELECT * FROM nodes
WHERE file_path LIKE ? OR file_path = ?
ORDER BY start_line
LIMIT 100`,
[`%${filePath}`, filePath]
);
}
/**
* List all indexed files, optionally filtered by language.
*/
export function listFiles(language?: string, limit = 50): CodeGraphQueryResult {
if (language) {
return queryDb(`SELECT * FROM files WHERE language = ? ORDER BY path LIMIT ?`, [
language,
limit,
]);
}
return queryDb(`SELECT * FROM files ORDER BY path LIMIT ?`, [limit]);
}
/**
* Check if CodeGraph DB is available.
*/
export function isCodeGraphAvailable(): boolean {
return getDbPath() !== null;
}
/**
* Get summary stats from the index.
*/
export function getCodeGraphStats(): CodeGraphQueryResult {
return queryDb(`SELECT 'total_nodes' as key, COUNT(*) as value FROM nodes UNION ALL
SELECT 'total_edges', COUNT(*) FROM edges UNION ALL
SELECT 'total_files', COUNT(*) FROM files UNION ALL
SELECT 'languages', GROUP_CONCAT(DISTINCT language) FROM files UNION ALL
SELECT 'node_kinds', GROUP_CONCAT(DISTINCT kind) FROM nodes`);
}