Re-architects MCP from one owner table + agent-written registration into an admin-curated catalog with two runtimes unioned per session, surfaced in the UI as "Connectors" (mcp/schema stays neutral, §0.1). Two runtimes behind one seam (§7): - Global runtime: shared, stateless connectors (web-search, Tavily…) on the HOST, connected at boot from mcp_global_servers, access-filtered per user via mcp_global_access. - Per-user runtime: a user's activated connectors run INSIDE their container, started at first login from mcp_user_servers and living until restart (§9); docker exec -i children die via kill_on_drop when the UserContext drops. - McpProvider trait (mcp/provider.rs): the session round-loop never learns which runtime owns a server. McpManager implements it directly (inert ownerless bundle); UserMcpView implements global ∪ user with an accessible_global snapshot. Both share McpManager::connect_all; McpServerSpec + global_row_spec/user_row_spec turn a DB row into a connectable spec. - mcp-client: McpServerConfig.launch_in runs a stdio command inside a container via docker exec -i (set at runtime, never parsed from config). Authorization is a capability on the role, not `if role==admin` (§0.1/§14): role_capabilities table + db/role_capabilities.rs — register_remote and register_local_from_catalog are self-service (seeded on every new role), while register_local_script and manage_catalog are admin-only. admin holds every capability by construction. This removes the agent-facing register_mcp/delete_mcp tools and the mcp kinds of list_items/toggle_item, closing the §14 RCE vector. Schema: - Registry: mcp_catalog (vetted templates — schema only, no live creds), mcp_global_servers + mcp_global_access, role_capabilities. - Owner: mcp_user_servers (per-user activations; api_key encrypted at rest, catalog_name a bare TEXT snapshot, never an owner→registry FK). - Drops the old owner table mcp_servers. API + UI: src/frontend/api/mcp.rs (admin catalog/global/access + user available/activate/activated, all capability-gated via require_cap); web/components/connectors.js (<connectors-page>) renders the user view always and the admin view for role_id === 'admin'. Deferred: interactive per-user auth (OAuth callback / QR / SSH elicitation, §15) — only none/api_key wired; no boot seed of catalog presets; per-(user, session) MCP grant model still open.
97 lines
5.0 KiB
JavaScript
97 lines
5.0 KiB
JavaScript
import { AppTopbar } from './components/topbar.js';
|
|
import { AppSidebar } from './components/sidebar.js';
|
|
import { AppCopilot } from './components/copilot.js';
|
|
import { LlmProvidersPage } from './components/llm-providers.js';
|
|
import { ModelsHubPage } from './components/models-hub.js';
|
|
import { ModelsLlmSection } from './components/models-llm.js';
|
|
import { ModelsTranscribeSection } from './components/models-transcribe.js';
|
|
import { ModelsImageSection } from './components/models-image.js';
|
|
import { ModelsTtsSection } from './components/models-tts.js';
|
|
import { TasksPage } from './components/tasks/index.js';
|
|
import { AgentsPage } from './components/agents.js';
|
|
import { UsersPage } from './components/users-page.js';
|
|
import { RolesPage } from './components/roles-page.js';
|
|
import { ConnectorsPage } from './components/connectors.js';
|
|
import { ProfilePage } from './components/profile-page.js';
|
|
import { ApprovalGroupsPage } from './components/approval-groups.js';
|
|
import { ApprovalRulesPage } from './components/approval-rules.js';
|
|
import { ConfigPage } from './components/config-page.js';
|
|
import { AgentInboxPage } from './components/agent-inbox.js';
|
|
import { HomePage } from './components/home-page.js';
|
|
import { LlmRequestsPage } from './components/llm-requests.js';
|
|
import { LlmRequestDetail } from './components/llm-request-detail.js';
|
|
import { SessionDetailPage } from './components/session-detail.js';
|
|
import { TicSessionsPage } from './components/tic-sessions.js';
|
|
import { ProjectsPage } from './components/projects/index.js';
|
|
import { FileViewerPage } from './components/file-viewer-page.js';
|
|
import { SetupPage } from './components/setup-page.js';
|
|
import { LoginPage } from './components/login-page.js';
|
|
|
|
// Register the global `openFile(path)` helper (window.openFile → location.hash).
|
|
import './lib/open-file.js';
|
|
|
|
customElements.define('app-topbar', AppTopbar);
|
|
customElements.define('app-sidebar', AppSidebar);
|
|
customElements.define('app-copilot', AppCopilot);
|
|
customElements.define('llm-providers-page', LlmProvidersPage);
|
|
customElements.define('models-hub-page', ModelsHubPage);
|
|
customElements.define('models-llm-section', ModelsLlmSection);
|
|
customElements.define('models-transcribe-section', ModelsTranscribeSection);
|
|
customElements.define('models-image-section', ModelsImageSection);
|
|
customElements.define('models-tts-section', ModelsTtsSection);
|
|
customElements.define('tasks-page', TasksPage);
|
|
customElements.define('agents-page', AgentsPage);
|
|
customElements.define('users-page', UsersPage);
|
|
customElements.define('roles-page', RolesPage);
|
|
customElements.define('connectors-page', ConnectorsPage);
|
|
customElements.define('profile-page', ProfilePage);
|
|
customElements.define('approval-groups-page', ApprovalGroupsPage);
|
|
customElements.define('approval-rules-page', ApprovalRulesPage);
|
|
customElements.define('config-page', ConfigPage);
|
|
customElements.define('agent-inbox-page', AgentInboxPage);
|
|
customElements.define('home-page', HomePage);
|
|
customElements.define('llm-requests-page', LlmRequestsPage);
|
|
customElements.define('llm-request-detail', LlmRequestDetail);
|
|
customElements.define('session-detail-page', SessionDetailPage);
|
|
customElements.define('tic-sessions-page', TicSessionsPage);
|
|
customElements.define('projects-page', ProjectsPage);
|
|
customElements.define('file-viewer-page', FileViewerPage);
|
|
customElements.define('setup-page', SetupPage);
|
|
customElements.define('login-page', LoginPage);
|
|
|
|
// Toggle the workspace placeholder when an LLM page opens/closes.
|
|
const workspace = document.getElementById('app-workspace');
|
|
window.addEventListener('llm-page-change', (e) => {
|
|
workspace.style.display = e.detail.page ? 'none' : 'flex';
|
|
});
|
|
|
|
// ── First-run check ─────────────────────────────────────────────────────────
|
|
// If no user exists yet, show the setup screen. Otherwise, check if we have
|
|
// a valid session: if not, show the login screen. If logged in, show the app.
|
|
(async () => {
|
|
const app = document.getElementById('app');
|
|
const setup = document.querySelector('setup-page');
|
|
const login = document.querySelector('login-page');
|
|
|
|
try {
|
|
const setupRes = await fetch('/api/setup/status');
|
|
if (setupRes.ok) {
|
|
const { needs_setup } = await setupRes.json();
|
|
if (needs_setup) {
|
|
if (app) app.style.display = 'none';
|
|
if (setup) setup.style.display = '';
|
|
return;
|
|
}
|
|
}
|
|
} catch { /* fall through to auth check */ }
|
|
|
|
try {
|
|
const meRes = await fetch('/api/auth/me');
|
|
if (meRes.status === 401) {
|
|
if (app) app.style.display = 'none';
|
|
if (login) login.style.display = '';
|
|
return;
|
|
}
|
|
} catch { /* show app by default */ }
|
|
})();
|