Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ import { getAgentPermissions, type Permission } from './lib/iam'
import { detectRequiredPermissions } from './lib/sql-permissions'
import { buildExecutableSql, formatExecutionError } from './lib/execute-sql'
import { auditSQL } from './lib/audit'
import { MCP_CATALOG_PAGE_SIZE, MCP_MAX_RESULT_ROWS } from '../src/lib/constants'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Server importing from src/lib/ without tooling guard

server/ code now depends on a path inside src/lib/, which is typically the client-side tree. The file's own comment asks it to stay dependency-free, but there is no ESLint boundary rule or separate tsconfig path alias enforcing that invariant. If a future contributor adds any browser-only import to src/lib/constants.ts (or even accidentally imports from another src/lib/ module that does), the server will break at startup — and the error would only surface at runtime or in CI, not at edit time. A shared/ or common/ directory at the root, or a lint rule banning server/** from importing src/**, would make the constraint machine-checked rather than comment-enforced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasonable hardening idea, but worth noting this isn't a new boundary: server/ already imports from src/lib/ today — e.g. server/lib/sql-permissions.ts imports src/lib/sql/core and src/lib/sql/pg-system-functions. So this PR follows the established pattern rather than introducing the cross-tree dependency, and src/lib/constants.ts is pure literal export consts (no imports of its own), so the dependency-free invariant holds today.

Machine-enforcing it (an ESLint no-restricted-imports rule banning browser-only imports from server-reachable src/lib modules, or relocating the genuinely-shared pieces to a root shared/ dir) is a good idea — but it's a repo-wide change that should also cover the existing sql/* imports, so I'd do it as a separate follow-up rather than scope-creep this constants PR. Leaving as-is here.


declare const __APP_VERSION__: string

export const MCP_PATH = '/mcp'
const PAGE_SIZE = 100
const PAGE_SIZE = MCP_CATALOG_PAGE_SIZE

// Hard cap on rows returned by the execution tools, so a broad SELECT can't flood an agent's
// context. The full result is fetched, then capped; a `truncated` flag tells the agent to narrow
// (LIMIT/WHERE or a smaller maxRows). MCP only — the UI route is intentionally uncapped because it
// needs the full result set for CSV export and inline editing.
export const MAX_RESULT_ROWS = 1000
export const MAX_RESULT_ROWS = MCP_MAX_RESULT_ROWS

// A resolved MCP caller — identity + audit actor for one agent. Permission resolution
// itself lives in iam.ts (getAgentPermissions), the single home for that decision.
Expand Down
5 changes: 3 additions & 2 deletions server/services/query-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { hasPermission, requirePermission, requirePermissions, requireAnyPermiss
import { detectRequiredPermissions } from "../lib/sql-permissions";
import { buildExecutableSql, formatExecutionError } from "../lib/execute-sql";
import { auditSQL, auditExport, listAuditEvents, listSystemAuditEvents, type AuditEvent } from "../lib/audit";
import { AUDIT_LOG_FETCH_LIMIT, AUDIT_LOG_DEFAULT_LIMIT } from "../../src/lib/constants";

// Track active queries by queryId -> { pid, connectionDetails, email }
const activeQueries = new Map<string, { pid: number; details: ConnectionDetails; email: string }>();
Expand Down Expand Up @@ -1223,7 +1224,7 @@ export const queryServiceHandlers: ServiceImpl<typeof QueryService> = {
requirePermission(user, req.connectionId, 'admin', 'view audit log');
getConnectionDetails(req.connectionId);

const limit = req.limit > 0 ? Math.min(req.limit, 1000) : 100;
const limit = req.limit > 0 ? Math.min(req.limit, AUDIT_LOG_FETCH_LIMIT) : AUDIT_LOG_DEFAULT_LIMIT;
const entries = listAuditEvents(req.connectionId, limit).map(toAuditLogEntry);

return { entries };
Expand All @@ -1241,7 +1242,7 @@ export const queryServiceHandlers: ServiceImpl<typeof QueryService> = {
throw new ConnectError("Permission denied: viewing the system audit log requires instance owner", Code.PermissionDenied);
}

const limit = req.limit > 0 ? Math.min(req.limit, 1000) : 100;
const limit = req.limit > 0 ? Math.min(req.limit, AUDIT_LOG_FETCH_LIMIT) : AUDIT_LOG_DEFAULT_LIMIT;
const entries = listSystemAuditEvents(limit).map(toAuditLogEntry);

return { entries };
Expand Down
13 changes: 6 additions & 7 deletions src/hooks/useQuery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMutation, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query';
import { queryClient, connectionClient, aiClient } from '../lib/connect-client';
import { AUDIT_LOG_FETCH_LIMIT, LIVE_QUERY_REFETCH_INTERVAL_MS } from '../lib/constants';
import type { ColumnMetadata } from '../components/sql-editor/hooks/useEditorTabs';

// Query keys
Expand Down Expand Up @@ -309,7 +310,7 @@ export function useActiveProcesses(connectionId: string, enabled = true) {
return response.sessions;
},
enabled: enabled && !!connectionId,
refetchInterval: 5000,
refetchInterval: LIVE_QUERY_REFETCH_INTERVAL_MS,
});
}

Expand All @@ -328,17 +329,15 @@ export function useTerminateProcess() {
});
}

const AUDIT_LOG_LIMIT = 1000;

export function useAuditLogEntries(connectionId: string, enabled = true) {
return useQuery({
queryKey: queryKeys.auditLog(connectionId),
queryFn: async () => {
const response = await queryClient.getAuditLogEntries({ connectionId, limit: AUDIT_LOG_LIMIT });
const response = await queryClient.getAuditLogEntries({ connectionId, limit: AUDIT_LOG_FETCH_LIMIT });
return response.entries;
},
enabled: enabled && !!connectionId,
refetchInterval: 5000,
refetchInterval: LIVE_QUERY_REFETCH_INTERVAL_MS,
});
}

Expand All @@ -348,11 +347,11 @@ export function useSystemAuditLogEntries(enabled = true) {
return useQuery({
queryKey: queryKeys.systemAuditLog(),
queryFn: async () => {
const response = await queryClient.getSystemAuditLogEntries({ limit: AUDIT_LOG_LIMIT });
const response = await queryClient.getSystemAuditLogEntries({ limit: AUDIT_LOG_FETCH_LIMIT });
return response.entries;
},
enabled,
refetchInterval: 5000,
refetchInterval: LIVE_QUERY_REFETCH_INTERVAL_MS,
});
}

Expand Down
39 changes: 39 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Centralized home for cross-cutting and otherwise-duplicated constants.
//
// Keep this file dependency-free (pure values only) so both the client and the
// server can import it without dragging browser- or server-only code across the
// boundary. Feature-local constants that are used in exactly one place and are
// already well-named should stay next to their feature — only values that are
// duplicated, span the client/server boundary, or define a runtime contract
// belong here.

// --- Audit log ---

/**
* Max audit-log rows fetched per tab, and the server-side hard clamp on the
* `limit` request field. The client request and the server clamp must agree, so
* they share this single source of truth.
*/
export const AUDIT_LOG_FETCH_LIMIT = 1000

/** Server default applied when an audit-log request omits a positive `limit`. */
export const AUDIT_LOG_DEFAULT_LIMIT = 100

// --- Live polling ---

/**
* Refetch cadence (ms) for client queries that poll live data — active sessions
* and the audit-log tabs.
*/
export const LIVE_QUERY_REFETCH_INTERVAL_MS = 5000

// --- MCP server ---

/**
* Hard cap on rows an MCP execution tool returns. The full result is fetched,
* then capped; when capped the response sets `truncated` so the agent can narrow.
*/
export const MCP_MAX_RESULT_ROWS = 1000

/** Page size for MCP catalog browsing (`list_objects` pagination). */
export const MCP_CATALOG_PAGE_SIZE = 100
Loading