@xenosystem/agent-interface-host 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +67 -0
- package/dist/acp/AcpAgentRegistryService.d.ts +77 -0
- package/dist/acp/AcpAgentRegistryService.d.ts.map +1 -0
- package/dist/acp/AcpAgentRegistryService.js +754 -0
- package/dist/acp/AcpAgentRegistryService.js.map +1 -0
- package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts +56 -0
- package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts.map +1 -0
- package/dist/acp/AcpAgentTurnExecutionAdapter.js +323 -0
- package/dist/acp/AcpAgentTurnExecutionAdapter.js.map +1 -0
- package/dist/acp/AcpApprovalPortAdapter.d.ts +41 -0
- package/dist/acp/AcpApprovalPortAdapter.d.ts.map +1 -0
- package/dist/acp/AcpApprovalPortAdapter.js +82 -0
- package/dist/acp/AcpApprovalPortAdapter.js.map +1 -0
- package/dist/acp/AcpCoordinationBridge.d.ts +13 -0
- package/dist/acp/AcpCoordinationBridge.d.ts.map +1 -0
- package/dist/acp/AcpCoordinationBridge.js +13 -0
- package/dist/acp/AcpCoordinationBridge.js.map +1 -0
- package/dist/acp/AcpDiagnosticsService.d.ts +61 -0
- package/dist/acp/AcpDiagnosticsService.d.ts.map +1 -0
- package/dist/acp/AcpDiagnosticsService.js +203 -0
- package/dist/acp/AcpDiagnosticsService.js.map +1 -0
- package/dist/acp/AcpExternalCertificationHarness.d.ts +47 -0
- package/dist/acp/AcpExternalCertificationHarness.d.ts.map +1 -0
- package/dist/acp/AcpExternalCertificationHarness.js +176 -0
- package/dist/acp/AcpExternalCertificationHarness.js.map +1 -0
- package/dist/acp/AcpHostBridge.d.ts +81 -0
- package/dist/acp/AcpHostBridge.d.ts.map +1 -0
- package/dist/acp/AcpHostBridge.js +101 -0
- package/dist/acp/AcpHostBridge.js.map +1 -0
- package/dist/acp/AcpInteractivePermissionBridge.d.ts +77 -0
- package/dist/acp/AcpInteractivePermissionBridge.d.ts.map +1 -0
- package/dist/acp/AcpInteractivePermissionBridge.js +127 -0
- package/dist/acp/AcpInteractivePermissionBridge.js.map +1 -0
- package/dist/acp/AcpPermissionBridge.d.ts +19 -0
- package/dist/acp/AcpPermissionBridge.d.ts.map +1 -0
- package/dist/acp/AcpPermissionBridge.js +110 -0
- package/dist/acp/AcpPermissionBridge.js.map +1 -0
- package/dist/acp/AcpPermissionPolicy.d.ts +111 -0
- package/dist/acp/AcpPermissionPolicy.d.ts.map +1 -0
- package/dist/acp/AcpPermissionPolicy.js +167 -0
- package/dist/acp/AcpPermissionPolicy.js.map +1 -0
- package/dist/acp/AcpProcessSupervisor.d.ts +134 -0
- package/dist/acp/AcpProcessSupervisor.d.ts.map +1 -0
- package/dist/acp/AcpProcessSupervisor.js +674 -0
- package/dist/acp/AcpProcessSupervisor.js.map +1 -0
- package/dist/acp/AcpProviderApprovalService.d.ts +27 -0
- package/dist/acp/AcpProviderApprovalService.d.ts.map +1 -0
- package/dist/acp/AcpProviderApprovalService.js +128 -0
- package/dist/acp/AcpProviderApprovalService.js.map +1 -0
- package/dist/acp/AcpProviderCatalogService.d.ts +69 -0
- package/dist/acp/AcpProviderCatalogService.d.ts.map +1 -0
- package/dist/acp/AcpProviderCatalogService.js +707 -0
- package/dist/acp/AcpProviderCatalogService.js.map +1 -0
- package/dist/acp/AcpProviderModelService.d.ts +6 -0
- package/dist/acp/AcpProviderModelService.d.ts.map +1 -0
- package/dist/acp/AcpProviderModelService.js +274 -0
- package/dist/acp/AcpProviderModelService.js.map +1 -0
- package/dist/acp/AcpProviderVersionReadiness.d.ts +6 -0
- package/dist/acp/AcpProviderVersionReadiness.d.ts.map +1 -0
- package/dist/acp/AcpProviderVersionReadiness.js +100 -0
- package/dist/acp/AcpProviderVersionReadiness.js.map +1 -0
- package/dist/acp/AcpRuntimeEventMapper.d.ts +16 -0
- package/dist/acp/AcpRuntimeEventMapper.d.ts.map +1 -0
- package/dist/acp/AcpRuntimeEventMapper.js +326 -0
- package/dist/acp/AcpRuntimeEventMapper.js.map +1 -0
- package/dist/acp/AcpSessionService.d.ts +65 -0
- package/dist/acp/AcpSessionService.d.ts.map +1 -0
- package/dist/acp/AcpSessionService.js +931 -0
- package/dist/acp/AcpSessionService.js.map +1 -0
- package/dist/acp/DurableAcpPermissionPolicyStore.d.ts +25 -0
- package/dist/acp/DurableAcpPermissionPolicyStore.d.ts.map +1 -0
- package/dist/acp/DurableAcpPermissionPolicyStore.js +100 -0
- package/dist/acp/DurableAcpPermissionPolicyStore.js.map +1 -0
- package/dist/acp/acpCommandSafety.d.ts +63 -0
- package/dist/acp/acpCommandSafety.d.ts.map +1 -0
- package/dist/acp/acpCommandSafety.js +90 -0
- package/dist/acp/acpCommandSafety.js.map +1 -0
- package/dist/acp/acpErrors.d.ts +47 -0
- package/dist/acp/acpErrors.d.ts.map +1 -0
- package/dist/acp/acpErrors.js +93 -0
- package/dist/acp/acpErrors.js.map +1 -0
- package/dist/acp/acpRedaction.d.ts +9 -0
- package/dist/acp/acpRedaction.d.ts.map +1 -0
- package/dist/acp/acpRedaction.js +69 -0
- package/dist/acp/acpRedaction.js.map +1 -0
- package/dist/acp/acpSessionResume.d.ts +8 -0
- package/dist/acp/acpSessionResume.d.ts.map +1 -0
- package/dist/acp/acpSessionResume.js +31 -0
- package/dist/acp/acpSessionResume.js.map +1 -0
- package/dist/acp/acpTypes.d.ts +467 -0
- package/dist/acp/acpTypes.d.ts.map +1 -0
- package/dist/acp/acpTypes.js +2 -0
- package/dist/acp/acpTypes.js.map +1 -0
- package/dist/agent/chooseDirectoryBridge.d.ts +29 -0
- package/dist/agent/chooseDirectoryBridge.d.ts.map +1 -0
- package/dist/agent/chooseDirectoryBridge.js +116 -0
- package/dist/agent/chooseDirectoryBridge.js.map +1 -0
- package/dist/agent/permissionProfiles.d.ts +29 -0
- package/dist/agent/permissionProfiles.d.ts.map +1 -0
- package/dist/agent/permissionProfiles.js +107 -0
- package/dist/agent/permissionProfiles.js.map +1 -0
- package/dist/agent/runtimeAuthorityPrompt.d.ts +13 -0
- package/dist/agent/runtimeAuthorityPrompt.d.ts.map +1 -0
- package/dist/agent/runtimeAuthorityPrompt.js +45 -0
- package/dist/agent/runtimeAuthorityPrompt.js.map +1 -0
- package/dist/agent/runtimeModeOutputGuard.d.ts +6 -0
- package/dist/agent/runtimeModeOutputGuard.d.ts.map +1 -0
- package/dist/agent/runtimeModeOutputGuard.js +40 -0
- package/dist/agent/runtimeModeOutputGuard.js.map +1 -0
- package/dist/agent/workspacePlacementBridge.d.ts +29 -0
- package/dist/agent/workspacePlacementBridge.d.ts.map +1 -0
- package/dist/agent/workspacePlacementBridge.js +143 -0
- package/dist/agent/workspacePlacementBridge.js.map +1 -0
- package/dist/agentSearch/AgentSearchService.d.ts +81 -0
- package/dist/agentSearch/AgentSearchService.d.ts.map +1 -0
- package/dist/agentSearch/AgentSearchService.js +1038 -0
- package/dist/agentSearch/AgentSearchService.js.map +1 -0
- package/dist/agentSearch/JsonInvertedIndexAdapter.d.ts +44 -0
- package/dist/agentSearch/JsonInvertedIndexAdapter.d.ts.map +1 -0
- package/dist/agentSearch/JsonInvertedIndexAdapter.js +307 -0
- package/dist/agentSearch/JsonInvertedIndexAdapter.js.map +1 -0
- package/dist/agentSearch/SearchIndexAdapter.d.ts +33 -0
- package/dist/agentSearch/SearchIndexAdapter.d.ts.map +1 -0
- package/dist/agentSearch/SearchIndexAdapter.js +2 -0
- package/dist/agentSearch/SearchIndexAdapter.js.map +1 -0
- package/dist/agentSearch/SearchSourceWatcher.d.ts +29 -0
- package/dist/agentSearch/SearchSourceWatcher.d.ts.map +1 -0
- package/dist/agentSearch/SearchSourceWatcher.js +150 -0
- package/dist/agentSearch/SearchSourceWatcher.js.map +1 -0
- package/dist/agentSearch/SqliteFtsIndexAdapter.d.ts +58 -0
- package/dist/agentSearch/SqliteFtsIndexAdapter.d.ts.map +1 -0
- package/dist/agentSearch/SqliteFtsIndexAdapter.js +420 -0
- package/dist/agentSearch/SqliteFtsIndexAdapter.js.map +1 -0
- package/dist/agentSearch/conversationIndexer.d.ts +69 -0
- package/dist/agentSearch/conversationIndexer.d.ts.map +1 -0
- package/dist/agentSearch/conversationIndexer.js +313 -0
- package/dist/agentSearch/conversationIndexer.js.map +1 -0
- package/dist/agentSearch/searchTypes.d.ts +264 -0
- package/dist/agentSearch/searchTypes.d.ts.map +1 -0
- package/dist/agentSearch/searchTypes.js +2 -0
- package/dist/agentSearch/searchTypes.js.map +1 -0
- package/dist/agentSearch/searchUtils.d.ts +12 -0
- package/dist/agentSearch/searchUtils.d.ts.map +1 -0
- package/dist/agentSearch/searchUtils.js +125 -0
- package/dist/agentSearch/searchUtils.js.map +1 -0
- package/dist/agentSearch/workspaceMetadataIndexer.d.ts +3 -0
- package/dist/agentSearch/workspaceMetadataIndexer.d.ts.map +1 -0
- package/dist/agentSearch/workspaceMetadataIndexer.js +125 -0
- package/dist/agentSearch/workspaceMetadataIndexer.js.map +1 -0
- package/dist/cli/CliControlPlaneService.d.ts +269 -0
- package/dist/cli/CliControlPlaneService.d.ts.map +1 -0
- package/dist/cli/CliControlPlaneService.js +1874 -0
- package/dist/cli/CliControlPlaneService.js.map +1 -0
- package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.d.ts +91 -0
- package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.d.ts.map +1 -0
- package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.js +379 -0
- package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.js.map +1 -0
- package/dist/codebase/CodebaseIntelligenceService.d.ts +68 -0
- package/dist/codebase/CodebaseIntelligenceService.d.ts.map +1 -0
- package/dist/codebase/CodebaseIntelligenceService.js +1717 -0
- package/dist/codebase/CodebaseIntelligenceService.js.map +1 -0
- package/dist/codebase/LanguageServerProcessManager.d.ts +85 -0
- package/dist/codebase/LanguageServerProcessManager.d.ts.map +1 -0
- package/dist/codebase/LanguageServerProcessManager.js +607 -0
- package/dist/codebase/LanguageServerProcessManager.js.map +1 -0
- package/dist/codebase/codeTypes.d.ts +485 -0
- package/dist/codebase/codeTypes.d.ts.map +1 -0
- package/dist/codebase/codeTypes.js +2 -0
- package/dist/codebase/codeTypes.js.map +1 -0
- package/dist/codebase/codeUtils.d.ts +24 -0
- package/dist/codebase/codeUtils.d.ts.map +1 -0
- package/dist/codebase/codeUtils.js +177 -0
- package/dist/codebase/codeUtils.js.map +1 -0
- package/dist/codebase/contextPacks.d.ts +18 -0
- package/dist/codebase/contextPacks.d.ts.map +1 -0
- package/dist/codebase/contextPacks.js +180 -0
- package/dist/codebase/contextPacks.js.map +1 -0
- package/dist/codebase/dependencyGraph.d.ts +3 -0
- package/dist/codebase/dependencyGraph.d.ts.map +1 -0
- package/dist/codebase/dependencyGraph.js +209 -0
- package/dist/codebase/dependencyGraph.js.map +1 -0
- package/dist/codebase/fileIndexer.d.ts +27 -0
- package/dist/codebase/fileIndexer.d.ts.map +1 -0
- package/dist/codebase/fileIndexer.js +167 -0
- package/dist/codebase/fileIndexer.js.map +1 -0
- package/dist/codebase/symbolIndexer.d.ts +3 -0
- package/dist/codebase/symbolIndexer.d.ts.map +1 -0
- package/dist/codebase/symbolIndexer.js +184 -0
- package/dist/codebase/symbolIndexer.js.map +1 -0
- package/dist/codebase/whatChanged.d.ts +4 -0
- package/dist/codebase/whatChanged.d.ts.map +1 -0
- package/dist/codebase/whatChanged.js +138 -0
- package/dist/codebase/whatChanged.js.map +1 -0
- package/dist/coordination/CoordinationService.d.ts +166 -0
- package/dist/coordination/CoordinationService.d.ts.map +1 -0
- package/dist/coordination/CoordinationService.js +247 -0
- package/dist/coordination/CoordinationService.js.map +1 -0
- package/dist/coordination/claims.d.ts +122 -0
- package/dist/coordination/claims.d.ts.map +1 -0
- package/dist/coordination/claims.js +196 -0
- package/dist/coordination/claims.js.map +1 -0
- package/dist/coordination/coordinationTools.d.ts +40 -0
- package/dist/coordination/coordinationTools.d.ts.map +1 -0
- package/dist/coordination/coordinationTools.js +92 -0
- package/dist/coordination/coordinationTools.js.map +1 -0
- package/dist/coordination/laneBridge.d.ts +84 -0
- package/dist/coordination/laneBridge.d.ts.map +1 -0
- package/dist/coordination/laneBridge.js +86 -0
- package/dist/coordination/laneBridge.js.map +1 -0
- package/dist/coordination/messages.d.ts +225 -0
- package/dist/coordination/messages.d.ts.map +1 -0
- package/dist/coordination/messages.js +335 -0
- package/dist/coordination/messages.js.map +1 -0
- package/dist/coordination/permissionWaitRegistry.d.ts +32 -0
- package/dist/coordination/permissionWaitRegistry.d.ts.map +1 -0
- package/dist/coordination/permissionWaitRegistry.js +59 -0
- package/dist/coordination/permissionWaitRegistry.js.map +1 -0
- package/dist/coordination/query.d.ts +187 -0
- package/dist/coordination/query.d.ts.map +1 -0
- package/dist/coordination/query.js +138 -0
- package/dist/coordination/query.js.map +1 -0
- package/dist/coordination/renderInbox.d.ts +15 -0
- package/dist/coordination/renderInbox.d.ts.map +1 -0
- package/dist/coordination/renderInbox.js +19 -0
- package/dist/coordination/renderInbox.js.map +1 -0
- package/dist/coordination/runtimeEvents.d.ts +53 -0
- package/dist/coordination/runtimeEvents.d.ts.map +1 -0
- package/dist/coordination/runtimeEvents.js +112 -0
- package/dist/coordination/runtimeEvents.js.map +1 -0
- package/dist/detachedHostService.d.ts +77 -0
- package/dist/detachedHostService.d.ts.map +1 -0
- package/dist/detachedHostService.js +66 -0
- package/dist/detachedHostService.js.map +1 -0
- package/dist/detachedHostSupervisor.d.ts +102 -0
- package/dist/detachedHostSupervisor.d.ts.map +1 -0
- package/dist/detachedHostSupervisor.js +168 -0
- package/dist/detachedHostSupervisor.js.map +1 -0
- package/dist/durableRuntimeEventStore.d.ts +49 -0
- package/dist/durableRuntimeEventStore.d.ts.map +1 -0
- package/dist/durableRuntimeEventStore.js +364 -0
- package/dist/durableRuntimeEventStore.js.map +1 -0
- package/dist/engineering/EngineeringWorkbenchService.d.ts +142 -0
- package/dist/engineering/EngineeringWorkbenchService.d.ts.map +1 -0
- package/dist/engineering/EngineeringWorkbenchService.js +2301 -0
- package/dist/engineering/EngineeringWorkbenchService.js.map +1 -0
- package/dist/engineering/LandingService.d.ts +112 -0
- package/dist/engineering/LandingService.d.ts.map +1 -0
- package/dist/engineering/LandingService.js +239 -0
- package/dist/engineering/LandingService.js.map +1 -0
- package/dist/engineering/engineeringTypes.d.ts +2 -0
- package/dist/engineering/engineeringTypes.d.ts.map +1 -0
- package/dist/engineering/engineeringTypes.js +2 -0
- package/dist/engineering/engineeringTypes.js.map +1 -0
- package/dist/engineering/patchReview.d.ts +11 -0
- package/dist/engineering/patchReview.d.ts.map +1 -0
- package/dist/engineering/patchReview.js +174 -0
- package/dist/engineering/patchReview.js.map +1 -0
- package/dist/index.d.ts +834 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3242 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/IntakeService.d.ts +84 -0
- package/dist/intake/IntakeService.d.ts.map +1 -0
- package/dist/intake/IntakeService.js +117 -0
- package/dist/intake/IntakeService.js.map +1 -0
- package/dist/intake/RepositoryTodoIntakeSource.d.ts +72 -0
- package/dist/intake/RepositoryTodoIntakeSource.d.ts.map +1 -0
- package/dist/intake/RepositoryTodoIntakeSource.js +168 -0
- package/dist/intake/RepositoryTodoIntakeSource.js.map +1 -0
- package/dist/intake/WorkspaceTodoIntakeSource.d.ts +53 -0
- package/dist/intake/WorkspaceTodoIntakeSource.d.ts.map +1 -0
- package/dist/intake/WorkspaceTodoIntakeSource.js +81 -0
- package/dist/intake/WorkspaceTodoIntakeSource.js.map +1 -0
- package/dist/isolation/CommandExecution.d.ts +132 -0
- package/dist/isolation/CommandExecution.d.ts.map +1 -0
- package/dist/isolation/CommandExecution.js +48 -0
- package/dist/isolation/CommandExecution.js.map +1 -0
- package/dist/isolation/LocalCommandExecutor.d.ts +27 -0
- package/dist/isolation/LocalCommandExecutor.d.ts.map +1 -0
- package/dist/isolation/LocalCommandExecutor.js +97 -0
- package/dist/isolation/LocalCommandExecutor.js.map +1 -0
- package/dist/isolation/RunExecutionEnvironment.d.ts +135 -0
- package/dist/isolation/RunExecutionEnvironment.d.ts.map +1 -0
- package/dist/isolation/RunExecutionEnvironment.js +206 -0
- package/dist/isolation/RunExecutionEnvironment.js.map +1 -0
- package/dist/isolation/RunIsolationService.d.ts +90 -0
- package/dist/isolation/RunIsolationService.d.ts.map +1 -0
- package/dist/isolation/RunIsolationService.js +86 -0
- package/dist/isolation/RunIsolationService.js.map +1 -0
- package/dist/isolation/SandboxCommandExecutor.d.ts +93 -0
- package/dist/isolation/SandboxCommandExecutor.d.ts.map +1 -0
- package/dist/isolation/SandboxCommandExecutor.js +189 -0
- package/dist/isolation/SandboxCommandExecutor.js.map +1 -0
- package/dist/knowledge/WorkspaceKnowledgeService.d.ts +71 -0
- package/dist/knowledge/WorkspaceKnowledgeService.d.ts.map +1 -0
- package/dist/knowledge/WorkspaceKnowledgeService.js +103 -0
- package/dist/knowledge/WorkspaceKnowledgeService.js.map +1 -0
- package/dist/localHostAuthority.d.ts +24 -0
- package/dist/localHostAuthority.d.ts.map +1 -0
- package/dist/localHostAuthority.js +87 -0
- package/dist/localHostAuthority.js.map +1 -0
- package/dist/localHostLease.d.ts +51 -0
- package/dist/localHostLease.d.ts.map +1 -0
- package/dist/localHostLease.js +167 -0
- package/dist/localHostLease.js.map +1 -0
- package/dist/localHostPaths.d.ts +30 -0
- package/dist/localHostPaths.d.ts.map +1 -0
- package/dist/localHostPaths.js +60 -0
- package/dist/localHostPaths.js.map +1 -0
- package/dist/localRpcTransport.d.ts +21 -0
- package/dist/localRpcTransport.d.ts.map +1 -0
- package/dist/localRpcTransport.js +423 -0
- package/dist/localRpcTransport.js.map +1 -0
- package/dist/node.d.ts +104 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +103 -0
- package/dist/node.js.map +1 -0
- package/dist/observation/DirectoryBaselineScanner.d.ts +44 -0
- package/dist/observation/DirectoryBaselineScanner.d.ts.map +1 -0
- package/dist/observation/DirectoryBaselineScanner.js +112 -0
- package/dist/observation/DirectoryBaselineScanner.js.map +1 -0
- package/dist/observation/MountContentReader.d.ts +63 -0
- package/dist/observation/MountContentReader.d.ts.map +1 -0
- package/dist/observation/MountContentReader.js +158 -0
- package/dist/observation/MountContentReader.js.map +1 -0
- package/dist/observation/RunObservationService.d.ts +229 -0
- package/dist/observation/RunObservationService.d.ts.map +1 -0
- package/dist/observation/RunObservationService.js +248 -0
- package/dist/observation/RunObservationService.js.map +1 -0
- package/dist/observation/XenoUseObservationSource.d.ts +42 -0
- package/dist/observation/XenoUseObservationSource.d.ts.map +1 -0
- package/dist/observation/XenoUseObservationSource.js +104 -0
- package/dist/observation/XenoUseObservationSource.js.map +1 -0
- package/dist/observation/XenoUseRunObserver.d.ts +140 -0
- package/dist/observation/XenoUseRunObserver.d.ts.map +1 -0
- package/dist/observation/XenoUseRunObserver.js +256 -0
- package/dist/observation/XenoUseRunObserver.js.map +1 -0
- package/dist/pluginMcp/PluginMcpManagerService.d.ts +141 -0
- package/dist/pluginMcp/PluginMcpManagerService.d.ts.map +1 -0
- package/dist/pluginMcp/PluginMcpManagerService.js +1821 -0
- package/dist/pluginMcp/PluginMcpManagerService.js.map +1 -0
- package/dist/pluginMcp/pluginMcpTypes.d.ts +349 -0
- package/dist/pluginMcp/pluginMcpTypes.d.ts.map +1 -0
- package/dist/pluginMcp/pluginMcpTypes.js +2 -0
- package/dist/pluginMcp/pluginMcpTypes.js.map +1 -0
- package/dist/policy/WorkspacePolicyService.d.ts +83 -0
- package/dist/policy/WorkspacePolicyService.d.ts.map +1 -0
- package/dist/policy/WorkspacePolicyService.js +809 -0
- package/dist/policy/WorkspacePolicyService.js.map +1 -0
- package/dist/policy/policyGuard.d.ts +27 -0
- package/dist/policy/policyGuard.d.ts.map +1 -0
- package/dist/policy/policyGuard.js +143 -0
- package/dist/policy/policyGuard.js.map +1 -0
- package/dist/policy/policyTypes.d.ts +190 -0
- package/dist/policy/policyTypes.d.ts.map +1 -0
- package/dist/policy/policyTypes.js +2 -0
- package/dist/policy/policyTypes.js.map +1 -0
- package/dist/runtime/RuntimeEventStore.d.ts +42 -0
- package/dist/runtime/RuntimeEventStore.d.ts.map +1 -0
- package/dist/runtime/RuntimeEventStore.js +412 -0
- package/dist/runtime/RuntimeEventStore.js.map +1 -0
- package/dist/runtime/RuntimeEventStoreRepositoryAdapter.d.ts +21 -0
- package/dist/runtime/RuntimeEventStoreRepositoryAdapter.d.ts.map +1 -0
- package/dist/runtime/RuntimeEventStoreRepositoryAdapter.js +41 -0
- package/dist/runtime/RuntimeEventStoreRepositoryAdapter.js.map +1 -0
- package/dist/runtime/RuntimeTimelineProjection.d.ts +5 -0
- package/dist/runtime/RuntimeTimelineProjection.d.ts.map +1 -0
- package/dist/runtime/RuntimeTimelineProjection.js +705 -0
- package/dist/runtime/RuntimeTimelineProjection.js.map +1 -0
- package/dist/runtime/runtimeEventTypes.d.ts +73 -0
- package/dist/runtime/runtimeEventTypes.d.ts.map +1 -0
- package/dist/runtime/runtimeEventTypes.js +2 -0
- package/dist/runtime/runtimeEventTypes.js.map +1 -0
- package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts +117 -0
- package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts.map +1 -0
- package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js +214 -0
- package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js.map +1 -0
- package/dist/session/AgentSessionService.d.ts +285 -0
- package/dist/session/AgentSessionService.d.ts.map +1 -0
- package/dist/session/AgentSessionService.js +444 -0
- package/dist/session/AgentSessionService.js.map +1 -0
- package/dist/session/bufferedSessionPty.d.ts +27 -0
- package/dist/session/bufferedSessionPty.d.ts.map +1 -0
- package/dist/session/bufferedSessionPty.js +49 -0
- package/dist/session/bufferedSessionPty.js.map +1 -0
- package/dist/session/renderSupport.d.ts +53 -0
- package/dist/session/renderSupport.d.ts.map +1 -0
- package/dist/session/renderSupport.js +41 -0
- package/dist/session/renderSupport.js.map +1 -0
- package/dist/session/runtimeEventChannel.d.ts +56 -0
- package/dist/session/runtimeEventChannel.d.ts.map +1 -0
- package/dist/session/runtimeEventChannel.js +53 -0
- package/dist/session/runtimeEventChannel.js.map +1 -0
- package/dist/state/JsonAgentStateRepository.d.ts +29 -0
- package/dist/state/JsonAgentStateRepository.d.ts.map +1 -0
- package/dist/state/JsonAgentStateRepository.js +165 -0
- package/dist/state/JsonAgentStateRepository.js.map +1 -0
- package/dist/state/SqliteAgentStateRepository.d.ts +58 -0
- package/dist/state/SqliteAgentStateRepository.d.ts.map +1 -0
- package/dist/state/SqliteAgentStateRepository.js +777 -0
- package/dist/state/SqliteAgentStateRepository.js.map +1 -0
- package/dist/state/createAgentStateRepository.d.ts +4 -0
- package/dist/state/createAgentStateRepository.d.ts.map +1 -0
- package/dist/state/createAgentStateRepository.js +12 -0
- package/dist/state/createAgentStateRepository.js.map +1 -0
- package/dist/state/stateSanitizers.d.ts +7 -0
- package/dist/state/stateSanitizers.d.ts.map +1 -0
- package/dist/state/stateSanitizers.js +87 -0
- package/dist/state/stateSanitizers.js.map +1 -0
- package/dist/subagents/AcpSubagentExecutionAdapter.d.ts +12 -0
- package/dist/subagents/AcpSubagentExecutionAdapter.d.ts.map +1 -0
- package/dist/subagents/AcpSubagentExecutionAdapter.js +119 -0
- package/dist/subagents/AcpSubagentExecutionAdapter.js.map +1 -0
- package/dist/subagents/CliSubagentExecutionAdapter.d.ts +13 -0
- package/dist/subagents/CliSubagentExecutionAdapter.d.ts.map +1 -0
- package/dist/subagents/CliSubagentExecutionAdapter.js +217 -0
- package/dist/subagents/CliSubagentExecutionAdapter.js.map +1 -0
- package/dist/subagents/HostTurnSubagentExecutionAdapter.d.ts +78 -0
- package/dist/subagents/HostTurnSubagentExecutionAdapter.d.ts.map +1 -0
- package/dist/subagents/HostTurnSubagentExecutionAdapter.js +160 -0
- package/dist/subagents/HostTurnSubagentExecutionAdapter.js.map +1 -0
- package/dist/subagents/SubagentRunService.d.ts +103 -0
- package/dist/subagents/SubagentRunService.d.ts.map +1 -0
- package/dist/subagents/SubagentRunService.js +1183 -0
- package/dist/subagents/SubagentRunService.js.map +1 -0
- package/dist/subagents/SubagentScheduler.d.ts +167 -0
- package/dist/subagents/SubagentScheduler.d.ts.map +1 -0
- package/dist/subagents/SubagentScheduler.js +530 -0
- package/dist/subagents/SubagentScheduler.js.map +1 -0
- package/dist/subagents/TeamAgentRouter.d.ts +104 -0
- package/dist/subagents/TeamAgentRouter.d.ts.map +1 -0
- package/dist/subagents/TeamAgentRouter.js +435 -0
- package/dist/subagents/TeamAgentRouter.js.map +1 -0
- package/dist/subagents/capabilityMatcher.d.ts +3 -0
- package/dist/subagents/capabilityMatcher.d.ts.map +1 -0
- package/dist/subagents/capabilityMatcher.js +127 -0
- package/dist/subagents/capabilityMatcher.js.map +1 -0
- package/dist/subagents/subagentRunPort.d.ts +43 -0
- package/dist/subagents/subagentRunPort.d.ts.map +1 -0
- package/dist/subagents/subagentRunPort.js +24 -0
- package/dist/subagents/subagentRunPort.js.map +1 -0
- package/dist/subagents/subagentTypes.d.ts +330 -0
- package/dist/subagents/subagentTypes.d.ts.map +1 -0
- package/dist/subagents/subagentTypes.js +2 -0
- package/dist/subagents/subagentTypes.js.map +1 -0
- package/dist/subagents/subagentWorkspaceAuthority.d.ts +29 -0
- package/dist/subagents/subagentWorkspaceAuthority.d.ts.map +1 -0
- package/dist/subagents/subagentWorkspaceAuthority.js +173 -0
- package/dist/subagents/subagentWorkspaceAuthority.js.map +1 -0
- package/dist/support/SupportDiagnosticsService.d.ts +87 -0
- package/dist/support/SupportDiagnosticsService.d.ts.map +1 -0
- package/dist/support/SupportDiagnosticsService.js +1199 -0
- package/dist/support/SupportDiagnosticsService.js.map +1 -0
- package/dist/turns/DurableInterruptedTurnStore.d.ts +14 -0
- package/dist/turns/DurableInterruptedTurnStore.d.ts.map +1 -0
- package/dist/turns/DurableInterruptedTurnStore.js +63 -0
- package/dist/turns/DurableInterruptedTurnStore.js.map +1 -0
- package/dist/turns/interruptedTurns.d.ts +50 -0
- package/dist/turns/interruptedTurns.d.ts.map +1 -0
- package/dist/turns/interruptedTurns.js +54 -0
- package/dist/turns/interruptedTurns.js.map +1 -0
- package/dist/turns/turnAdmission.d.ts +113 -0
- package/dist/turns/turnAdmission.d.ts.map +1 -0
- package/dist/turns/turnAdmission.js +136 -0
- package/dist/turns/turnAdmission.js.map +1 -0
- package/dist/workspace/WorkspaceHostService.d.ts +126 -0
- package/dist/workspace/WorkspaceHostService.d.ts.map +1 -0
- package/dist/workspace/WorkspaceHostService.js +841 -0
- package/dist/workspace/WorkspaceHostService.js.map +1 -0
- package/dist/workspace/WorkspaceShellService.d.ts +71 -0
- package/dist/workspace/WorkspaceShellService.d.ts.map +1 -0
- package/dist/workspace/WorkspaceShellService.js +273 -0
- package/dist/workspace/WorkspaceShellService.js.map +1 -0
- package/dist/workspace/fileAssociations.d.ts +12 -0
- package/dist/workspace/fileAssociations.d.ts.map +1 -0
- package/dist/workspace/fileAssociations.js +202 -0
- package/dist/workspace/fileAssociations.js.map +1 -0
- package/dist/workspace/scrollbackBuffer.d.ts +48 -0
- package/dist/workspace/scrollbackBuffer.d.ts.map +1 -0
- package/dist/workspace/scrollbackBuffer.js +94 -0
- package/dist/workspace/scrollbackBuffer.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,1821 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { dirname, join, resolve } from 'path';
|
|
5
|
+
import { createScopeAcl, estimateTokens, normalizeSearchText, stableDocumentId } from '../agentSearch/searchUtils.js';
|
|
6
|
+
const DEFAULT_REGISTRY_POLICY = 'context';
|
|
7
|
+
const MAX_PLUGIN_EVENTS = 500;
|
|
8
|
+
const MAX_PREVIEW_BYTES = 8_000;
|
|
9
|
+
class PluginMcpDeniedError extends Error {
|
|
10
|
+
denial;
|
|
11
|
+
constructor(denial) {
|
|
12
|
+
super(denial.message);
|
|
13
|
+
this.name = 'PluginMcpDeniedError';
|
|
14
|
+
this.denial = denial;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
let sdkModulePromise = null;
|
|
18
|
+
function loadSdk() {
|
|
19
|
+
if (!sdkModulePromise)
|
|
20
|
+
sdkModulePromise = import('@xenosystem/agent-sdk');
|
|
21
|
+
return sdkModulePromise;
|
|
22
|
+
}
|
|
23
|
+
function safeReadJson(path, fallback) {
|
|
24
|
+
try {
|
|
25
|
+
if (!existsSync(path))
|
|
26
|
+
return fallback;
|
|
27
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function ensureDir(path) {
|
|
34
|
+
if (!existsSync(path))
|
|
35
|
+
mkdirSync(path, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
function safeWriteJson(path, value) {
|
|
38
|
+
ensureDir(dirname(path));
|
|
39
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
40
|
+
writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf-8');
|
|
41
|
+
renameSync(tmp, path);
|
|
42
|
+
}
|
|
43
|
+
function optionalString(value) {
|
|
44
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
45
|
+
}
|
|
46
|
+
function stringArray(value) {
|
|
47
|
+
if (!Array.isArray(value))
|
|
48
|
+
return [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const entry of value) {
|
|
52
|
+
if (typeof entry !== 'string')
|
|
53
|
+
continue;
|
|
54
|
+
const trimmed = entry.trim();
|
|
55
|
+
const key = trimmed.toLowerCase();
|
|
56
|
+
if (!trimmed || seen.has(key))
|
|
57
|
+
continue;
|
|
58
|
+
seen.add(key);
|
|
59
|
+
out.push(trimmed);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function stringRecord(value) {
|
|
64
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
65
|
+
return {};
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
68
|
+
if (typeof entry !== 'string')
|
|
69
|
+
continue;
|
|
70
|
+
if (!key.trim())
|
|
71
|
+
continue;
|
|
72
|
+
out[key.trim()] = entry;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function numberTimestamp(value, fallback = Date.now()) {
|
|
77
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
78
|
+
}
|
|
79
|
+
function normalizeMcpScope(value) {
|
|
80
|
+
return value === 'project' || value === 'mcprc' || value === 'session' || value === 'user'
|
|
81
|
+
? value
|
|
82
|
+
: 'user';
|
|
83
|
+
}
|
|
84
|
+
function normalizeRegistryPolicy(value) {
|
|
85
|
+
return value === 'none' || value === 'prompts' || value === 'resources' || value === 'context' || value === 'all'
|
|
86
|
+
? value
|
|
87
|
+
: DEFAULT_REGISTRY_POLICY;
|
|
88
|
+
}
|
|
89
|
+
function normalizePolicyKey(value) {
|
|
90
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
91
|
+
}
|
|
92
|
+
function normalizedList(value) {
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const entry of stringArray(value)) {
|
|
96
|
+
const key = normalizePolicyKey(entry);
|
|
97
|
+
if (!key || seen.has(key))
|
|
98
|
+
continue;
|
|
99
|
+
seen.add(key);
|
|
100
|
+
out.push(entry.trim());
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function normalizeCaller(value) {
|
|
105
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
106
|
+
return {
|
|
107
|
+
workspaceId: optionalString(record.workspaceId),
|
|
108
|
+
teamId: optionalString(record.teamId),
|
|
109
|
+
agentId: optionalString(record.agentId),
|
|
110
|
+
allowedDirectories: stringArray(record.allowedDirectories),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function normalizePolicyScope(value) {
|
|
114
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
115
|
+
const type = record.type === 'workspace' || record.type === 'team' || record.type === 'agent' ? record.type : 'global';
|
|
116
|
+
return {
|
|
117
|
+
type,
|
|
118
|
+
workspaceId: optionalString(record.workspaceId),
|
|
119
|
+
teamId: optionalString(record.teamId),
|
|
120
|
+
agentId: optionalString(record.agentId),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function policyScopeId(scope) {
|
|
124
|
+
if (scope.type === 'agent')
|
|
125
|
+
return `agent:${scope.workspaceId || '*'}:${scope.teamId || '*'}:${scope.agentId || '*'}`;
|
|
126
|
+
if (scope.type === 'team')
|
|
127
|
+
return `team:${scope.workspaceId || '*'}:${scope.teamId || '*'}`;
|
|
128
|
+
if (scope.type === 'workspace')
|
|
129
|
+
return `workspace:${scope.workspaceId || '*'}`;
|
|
130
|
+
return 'global';
|
|
131
|
+
}
|
|
132
|
+
function scopeSpecificity(scope) {
|
|
133
|
+
if (scope.type === 'agent')
|
|
134
|
+
return 3;
|
|
135
|
+
if (scope.type === 'team')
|
|
136
|
+
return 2;
|
|
137
|
+
if (scope.type === 'workspace')
|
|
138
|
+
return 1;
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
function scopeMatchesCaller(scope, caller) {
|
|
142
|
+
if (scope.type === 'global')
|
|
143
|
+
return true;
|
|
144
|
+
if (scope.workspaceId && scope.workspaceId !== caller.workspaceId)
|
|
145
|
+
return false;
|
|
146
|
+
if (scope.type === 'workspace')
|
|
147
|
+
return !!caller.workspaceId;
|
|
148
|
+
if (scope.teamId && scope.teamId !== caller.teamId)
|
|
149
|
+
return false;
|
|
150
|
+
if (scope.type === 'team')
|
|
151
|
+
return !!caller.workspaceId && !!caller.teamId;
|
|
152
|
+
if (scope.agentId && scope.agentId !== caller.agentId)
|
|
153
|
+
return false;
|
|
154
|
+
return !!caller.workspaceId && !!caller.teamId && !!caller.agentId;
|
|
155
|
+
}
|
|
156
|
+
function normalizeScopedPolicy(input, existing) {
|
|
157
|
+
const scope = normalizePolicyScope(input.scope);
|
|
158
|
+
const id = optionalString(input.id) || existing?.id || policyScopeId(scope);
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
scope,
|
|
162
|
+
pluginAllowlist: normalizedList(input.pluginAllowlist ?? existing?.pluginAllowlist),
|
|
163
|
+
pluginDenylist: normalizedList(input.pluginDenylist ?? existing?.pluginDenylist),
|
|
164
|
+
mcpServerAllowlist: normalizedList(input.mcpServerAllowlist ?? existing?.mcpServerAllowlist),
|
|
165
|
+
mcpServerDenylist: normalizedList(input.mcpServerDenylist ?? existing?.mcpServerDenylist),
|
|
166
|
+
mcpRegistryPolicy: input.mcpRegistryPolicy !== undefined
|
|
167
|
+
? normalizeRegistryPolicy(input.mcpRegistryPolicy)
|
|
168
|
+
: existing?.mcpRegistryPolicy,
|
|
169
|
+
enabled: input.enabled ?? existing?.enabled ?? true,
|
|
170
|
+
updatedAt: numberTimestamp(input.updatedAt, existing?.updatedAt || Date.now()),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function listHas(list, value) {
|
|
174
|
+
const key = normalizePolicyKey(value);
|
|
175
|
+
return !!key && list.some((entry) => normalizePolicyKey(entry) === key);
|
|
176
|
+
}
|
|
177
|
+
function isAllowedByPolicyLists(value, allowlist, denylist) {
|
|
178
|
+
if (listHas(denylist, value))
|
|
179
|
+
return false;
|
|
180
|
+
if (allowlist.length > 0 && !listHas(allowlist, value))
|
|
181
|
+
return false;
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
function uniqueStrings(values) {
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const value of values) {
|
|
188
|
+
const trimmed = value.trim();
|
|
189
|
+
if (!trimmed || seen.has(trimmed))
|
|
190
|
+
continue;
|
|
191
|
+
seen.add(trimmed);
|
|
192
|
+
out.push(trimmed);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function mergeRemediation(values) {
|
|
197
|
+
const byCode = new Map();
|
|
198
|
+
for (const value of values) {
|
|
199
|
+
if (!byCode.has(value.code))
|
|
200
|
+
byCode.set(value.code, value);
|
|
201
|
+
}
|
|
202
|
+
return Array.from(byCode.values());
|
|
203
|
+
}
|
|
204
|
+
function remediation(code, label, description) {
|
|
205
|
+
return { code, label, description };
|
|
206
|
+
}
|
|
207
|
+
function createDenial(code, message, reasons, remediationItems) {
|
|
208
|
+
return {
|
|
209
|
+
code,
|
|
210
|
+
message,
|
|
211
|
+
reasons: uniqueStrings(reasons),
|
|
212
|
+
remediation: mergeRemediation(remediationItems),
|
|
213
|
+
safeForRenderer: true,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function trustRemediation(targetType, reasons) {
|
|
217
|
+
const text = reasons.join(' ').toLowerCase();
|
|
218
|
+
const items = [];
|
|
219
|
+
if (targetType === 'plugin') {
|
|
220
|
+
if (/not installed/.test(text))
|
|
221
|
+
items.push(remediation('install-plugin', 'Install plugin', 'Install the plugin before activating or exposing its tools.'));
|
|
222
|
+
if (/denied|blocked/.test(text))
|
|
223
|
+
items.push(remediation('approve-plugin', 'Approve plugin', 'Clear or approve the plugin trust decision after review.'));
|
|
224
|
+
if (/approval|review|unsigned|integrity|permission|changed/.test(text))
|
|
225
|
+
items.push(remediation('approve-plugin', 'Review plugin trust', 'Open Plugin Manager, review permissions, signature, and integrity, then approve if expected.'));
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
if (/not configured/.test(text))
|
|
229
|
+
items.push(remediation('configure-mcp', 'Configure MCP server', 'Add or import the MCP server in Hub before using it.'));
|
|
230
|
+
if (/disabled/.test(text))
|
|
231
|
+
items.push(remediation('enable-mcp', 'Enable MCP server', 'Enable the MCP server and reload approved MCP connections.'));
|
|
232
|
+
if (/scoped policy|deny/.test(text))
|
|
233
|
+
items.push(remediation('remove-deny-policy', 'Adjust scoped policy', 'Remove the deny rule or add this MCP server to the caller allowlist.'));
|
|
234
|
+
if (/registry policy/.test(text))
|
|
235
|
+
items.push(remediation('adjust-registry-policy', 'Adjust exposure policy', 'Change the MCP registry exposure to include this access type.'));
|
|
236
|
+
if (/config changed|config hash|requires review/.test(text))
|
|
237
|
+
items.push(remediation('review-config-drift', 'Review config drift', 'Review the changed command, URL, env, headers, or scope, then re-approve the MCP server.'));
|
|
238
|
+
if (/approval|not trusted|requires hub approval|denied/.test(text))
|
|
239
|
+
items.push(remediation('approve-mcp', 'Approve MCP server', 'Approve the MCP server in Hub after reviewing its transport, command, and endpoint.'));
|
|
240
|
+
}
|
|
241
|
+
return mergeRemediation(items.length > 0 ? items : [remediation('retry', 'Retry after remediation', 'Resolve the denial reason and retry the request.')]);
|
|
242
|
+
}
|
|
243
|
+
function pluginToolName(tool) {
|
|
244
|
+
const record = tool && typeof tool === 'object' ? tool : {};
|
|
245
|
+
return optionalString(record.definition?.name) || optionalString(record.name);
|
|
246
|
+
}
|
|
247
|
+
function isNpmPackageName(name) {
|
|
248
|
+
return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name);
|
|
249
|
+
}
|
|
250
|
+
function isMcpServerName(name) {
|
|
251
|
+
return /^[a-zA-Z0-9._-]{1,80}$/.test(name);
|
|
252
|
+
}
|
|
253
|
+
function isPathWithinAllowed(path, allowedDirectories) {
|
|
254
|
+
if (allowedDirectories.length === 0)
|
|
255
|
+
return false;
|
|
256
|
+
const resolved = resolve(path).toLowerCase().replace(/\\/g, '/');
|
|
257
|
+
return allowedDirectories.some((dir) => {
|
|
258
|
+
const allowed = resolve(dir).toLowerCase().replace(/\\/g, '/');
|
|
259
|
+
return resolved === allowed || resolved.startsWith(`${allowed}/`);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function hashJson(value) {
|
|
263
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
264
|
+
}
|
|
265
|
+
function sortedStringRecord(value) {
|
|
266
|
+
return Object.fromEntries(Object.entries(value || {}).sort(([left], [right]) => left.localeCompare(right)));
|
|
267
|
+
}
|
|
268
|
+
function mcpApprovalConfig(record) {
|
|
269
|
+
return {
|
|
270
|
+
name: record.name,
|
|
271
|
+
transport: record.transport,
|
|
272
|
+
command: record.command,
|
|
273
|
+
args: record.args,
|
|
274
|
+
env: sortedStringRecord(record.env),
|
|
275
|
+
cwd: record.cwd,
|
|
276
|
+
url: record.url,
|
|
277
|
+
headers: sortedStringRecord(record.headers),
|
|
278
|
+
disabled: record.disabled,
|
|
279
|
+
scope: record.scope,
|
|
280
|
+
source: record.source,
|
|
281
|
+
sourcePath: record.sourcePath,
|
|
282
|
+
approvalKey: record.approvalKey,
|
|
283
|
+
approvalRequired: record.approvalRequired,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function mcpConfigHash(record) {
|
|
287
|
+
return hashJson(mcpApprovalConfig(record));
|
|
288
|
+
}
|
|
289
|
+
function analyzePluginTrust(plugin, trustFile) {
|
|
290
|
+
const manifest = plugin.manifest;
|
|
291
|
+
const manifestHash = hashJson(manifest);
|
|
292
|
+
const integrityHash = plugin.integrityHash;
|
|
293
|
+
const decision = trustFile?.plugins?.[normalizePolicyKey(manifest.name)];
|
|
294
|
+
const permissionText = [
|
|
295
|
+
...stringArray(manifest.permissions),
|
|
296
|
+
...stringArray(manifest.capabilities),
|
|
297
|
+
].join(' ').toLowerCase();
|
|
298
|
+
const reasons = [];
|
|
299
|
+
const highRisk = /\b(shell|process|exec|filesystem|fs:write|storage:write|network|net|secrets|credentials)\b/.test(permissionText);
|
|
300
|
+
const mediumRisk = highRisk || /\b(tools:register|storage:read|panel|command|mcp)\b/.test(permissionText);
|
|
301
|
+
const signatureStatus = pluginSignatureStatus(plugin);
|
|
302
|
+
const riskLevel = highRisk ? 'high' : mediumRisk ? 'medium' : 'low';
|
|
303
|
+
if (signatureStatus !== 'verified')
|
|
304
|
+
reasons.push(signatureStatus === 'unsigned' ? 'Plugin manifest is unsigned.' : 'Plugin signature is present but not verified by Hub.');
|
|
305
|
+
if (highRisk)
|
|
306
|
+
reasons.push('Plugin requests high-risk permissions or capabilities.');
|
|
307
|
+
if (!integrityHash)
|
|
308
|
+
reasons.push('Plugin package integrity hash is missing.');
|
|
309
|
+
const trustDecisionHashMismatch = !!decision && (decision.manifestHash !== manifestHash
|
|
310
|
+
|| (!!integrityHash && decision.integrityHash !== integrityHash));
|
|
311
|
+
const trustDecisionMatchesCurrent = !!decision
|
|
312
|
+
&& decision.manifestHash === manifestHash
|
|
313
|
+
&& (!integrityHash || decision.integrityHash === integrityHash);
|
|
314
|
+
if (trustDecisionHashMismatch)
|
|
315
|
+
reasons.push('Plugin manifest or integrity changed after local trust decision.');
|
|
316
|
+
if (decision?.decision === 'denied' && trustDecisionMatchesCurrent) {
|
|
317
|
+
return {
|
|
318
|
+
state: 'blocked',
|
|
319
|
+
riskLevel,
|
|
320
|
+
signatureStatus,
|
|
321
|
+
integrityHash,
|
|
322
|
+
manifestHash,
|
|
323
|
+
deniedAt: decision.updatedAt,
|
|
324
|
+
reasons: [...reasons, 'Plugin was denied by local trust policy.'],
|
|
325
|
+
requiresApproval: true,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const approved = decision?.decision === 'approved'
|
|
329
|
+
&& trustDecisionMatchesCurrent;
|
|
330
|
+
const requiresApproval = trustDecisionHashMismatch || riskLevel !== 'low' || signatureStatus !== 'verified' || !integrityHash;
|
|
331
|
+
return {
|
|
332
|
+
state: approved || !requiresApproval ? 'trusted' : 'review-required',
|
|
333
|
+
riskLevel,
|
|
334
|
+
signatureStatus,
|
|
335
|
+
integrityHash,
|
|
336
|
+
manifestHash,
|
|
337
|
+
approvedAt: approved ? decision?.updatedAt : undefined,
|
|
338
|
+
reasons: reasons.length ? reasons : ['Plugin passed local trust checks.'],
|
|
339
|
+
requiresApproval: requiresApproval && !approved,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function pluginSignatureStatus(plugin) {
|
|
343
|
+
const record = plugin;
|
|
344
|
+
const manifest = plugin.manifest;
|
|
345
|
+
const signed = !!(record.signature
|
|
346
|
+
|| record.signatures
|
|
347
|
+
|| manifest.signature
|
|
348
|
+
|| manifest.signatures
|
|
349
|
+
|| manifest.publisherSignature);
|
|
350
|
+
if (record.signatureVerified === true || manifest.signatureVerified === true)
|
|
351
|
+
return 'verified';
|
|
352
|
+
return signed ? 'unknown' : 'unsigned';
|
|
353
|
+
}
|
|
354
|
+
function sanitizePluginInfo(plugin, trustFile) {
|
|
355
|
+
const manifest = plugin.manifest;
|
|
356
|
+
const trust = analyzePluginTrust(plugin, trustFile);
|
|
357
|
+
return {
|
|
358
|
+
name: manifest.name,
|
|
359
|
+
version: manifest.version,
|
|
360
|
+
description: manifest.description || '',
|
|
361
|
+
status: plugin.status,
|
|
362
|
+
installedAt: plugin.installedAt,
|
|
363
|
+
path: plugin.path,
|
|
364
|
+
capabilities: manifest.capabilities || [],
|
|
365
|
+
permissions: manifest.permissions || [],
|
|
366
|
+
tools: [
|
|
367
|
+
...stringArray(manifest.tools),
|
|
368
|
+
...stringArray(manifest.contributes?.tools?.map((tool) => tool.name)),
|
|
369
|
+
...stringArray(plugin.registeredTools?.map(pluginToolName)),
|
|
370
|
+
],
|
|
371
|
+
prompts: stringArray(manifest.contributes?.prompts?.map((prompt) => prompt.name)),
|
|
372
|
+
commands: stringArray(manifest.contributes?.commands?.map((command) => command.name)),
|
|
373
|
+
panels: stringArray(manifest.ui?.map((panel) => panel.id)),
|
|
374
|
+
keywords: stringArray(manifest.keywords),
|
|
375
|
+
error: plugin.error,
|
|
376
|
+
integrityHash: plugin.integrityHash,
|
|
377
|
+
trust,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function summarizeMcpTool(tool) {
|
|
381
|
+
return {
|
|
382
|
+
server: tool.server,
|
|
383
|
+
name: tool.name,
|
|
384
|
+
description: tool.description || '',
|
|
385
|
+
inputSchema: tool.inputSchema || {},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function summarizeMcpPrompt(prompt) {
|
|
389
|
+
return {
|
|
390
|
+
server: prompt.server,
|
|
391
|
+
name: prompt.name,
|
|
392
|
+
description: prompt.description || '',
|
|
393
|
+
arguments: Array.isArray(prompt.arguments) ? prompt.arguments : [],
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function summarizeMcpResource(resource) {
|
|
397
|
+
return {
|
|
398
|
+
server: resource.server,
|
|
399
|
+
uri: resource.uri,
|
|
400
|
+
name: resource.name,
|
|
401
|
+
description: resource.description,
|
|
402
|
+
mimeType: resource.mimeType,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function mcpRecordFromConfig(config, sourcePath, source) {
|
|
406
|
+
const now = Date.now();
|
|
407
|
+
const scope = normalizeMcpScope(config.scope);
|
|
408
|
+
return {
|
|
409
|
+
name: config.name,
|
|
410
|
+
transport: config.transport || (config.url ? 'sse' : 'stdio'),
|
|
411
|
+
command: config.command,
|
|
412
|
+
args: stringArray(config.args),
|
|
413
|
+
env: stringRecord(config.env),
|
|
414
|
+
cwd: config.cwd,
|
|
415
|
+
url: config.url,
|
|
416
|
+
headers: stringRecord(config.headers),
|
|
417
|
+
approvalRequired: config.approvalRequired ?? true,
|
|
418
|
+
disabled: !!config.disabled,
|
|
419
|
+
scope,
|
|
420
|
+
source,
|
|
421
|
+
sourcePath,
|
|
422
|
+
approvalKey: config.approvalKey || `${scope}:${config.name}`,
|
|
423
|
+
createdAt: now,
|
|
424
|
+
updatedAt: now,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function mcpRecordToConfig(record) {
|
|
428
|
+
return {
|
|
429
|
+
name: record.name,
|
|
430
|
+
transport: record.transport,
|
|
431
|
+
args: record.args,
|
|
432
|
+
env: record.env,
|
|
433
|
+
headers: record.headers,
|
|
434
|
+
approvalRequired: record.approvalRequired,
|
|
435
|
+
disabled: record.disabled,
|
|
436
|
+
scope: record.scope,
|
|
437
|
+
approvalKey: record.approvalKey,
|
|
438
|
+
...(record.command !== undefined ? { command: record.command } : {}),
|
|
439
|
+
...(record.cwd !== undefined ? { cwd: record.cwd } : {}),
|
|
440
|
+
...(record.url !== undefined ? { url: record.url } : {}),
|
|
441
|
+
...(record.approvalState !== undefined ? { approvalState: record.approvalState } : {}),
|
|
442
|
+
...(record.sourcePath !== undefined ? { sourcePath: record.sourcePath } : {}),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function analyzeMcpTrust(record) {
|
|
446
|
+
const reasons = [];
|
|
447
|
+
const highRisk = record.transport === 'stdio';
|
|
448
|
+
const riskLevel = highRisk ? 'high' : 'medium';
|
|
449
|
+
const configHash = mcpConfigHash(record);
|
|
450
|
+
const approvalMatchesConfig = record.approvalConfigHash === configHash;
|
|
451
|
+
const approved = record.approvalState === 'approved' && approvalMatchesConfig;
|
|
452
|
+
const denied = record.approvalState === 'denied' && approvalMatchesConfig;
|
|
453
|
+
if (record.disabled)
|
|
454
|
+
reasons.push('MCP server is disabled.');
|
|
455
|
+
if (!approved)
|
|
456
|
+
reasons.push('MCP server requires Hub approval before tool calls.');
|
|
457
|
+
if (record.approvalState === 'approved' && !approvalMatchesConfig) {
|
|
458
|
+
reasons.push('MCP server config hash drift detected: approved configuration no longer matches the current command, URL, env, headers, or scope. Review and re-approve before use.');
|
|
459
|
+
}
|
|
460
|
+
if (denied)
|
|
461
|
+
reasons.push('MCP server was denied by local approval policy.');
|
|
462
|
+
if (record.approvalState === 'denied' && !approvalMatchesConfig) {
|
|
463
|
+
reasons.push('MCP server config hash drift detected after denial. Review the changed configuration before clearing or approving.');
|
|
464
|
+
}
|
|
465
|
+
if (record.transport === 'stdio')
|
|
466
|
+
reasons.push('stdio MCP servers execute a local process.');
|
|
467
|
+
if (record.transport === 'sse')
|
|
468
|
+
reasons.push('SSE MCP servers communicate with a remote endpoint.');
|
|
469
|
+
return {
|
|
470
|
+
state: record.disabled || denied
|
|
471
|
+
? 'blocked'
|
|
472
|
+
: !approved
|
|
473
|
+
? 'review-required'
|
|
474
|
+
: 'trusted',
|
|
475
|
+
riskLevel,
|
|
476
|
+
signatureStatus: 'unknown',
|
|
477
|
+
manifestHash: configHash,
|
|
478
|
+
approvedConfigHash: record.approvalConfigHash,
|
|
479
|
+
currentConfigHash: configHash,
|
|
480
|
+
configDrift: !!record.approvalConfigHash && !approvalMatchesConfig,
|
|
481
|
+
approvedAt: approved ? record.updatedAt : undefined,
|
|
482
|
+
deniedAt: denied ? record.updatedAt : undefined,
|
|
483
|
+
reasons: reasons.length ? reasons : ['MCP server approved by local policy.'],
|
|
484
|
+
requiresApproval: !approved,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
export function createResultPreview(result, title, source = 'mcp-tool') {
|
|
488
|
+
const extracted = extractPreviewText(result);
|
|
489
|
+
const byteLength = Buffer.byteLength(extracted.text, 'utf-8');
|
|
490
|
+
const truncated = byteLength > MAX_PREVIEW_BYTES;
|
|
491
|
+
const textPreview = truncated
|
|
492
|
+
? extracted.text.slice(0, MAX_PREVIEW_BYTES) + '\n...[truncated]'
|
|
493
|
+
: extracted.text;
|
|
494
|
+
const previewByteLength = Buffer.byteLength(textPreview, 'utf-8');
|
|
495
|
+
const resultHash = createHash('sha256').update(extracted.text).digest('hex');
|
|
496
|
+
return {
|
|
497
|
+
kind: extracted.kind,
|
|
498
|
+
title,
|
|
499
|
+
textPreview,
|
|
500
|
+
byteLength,
|
|
501
|
+
itemCount: extracted.itemCount,
|
|
502
|
+
truncated,
|
|
503
|
+
metadata: {
|
|
504
|
+
safeForRenderer: true,
|
|
505
|
+
generatedAt: new Date().toISOString(),
|
|
506
|
+
source,
|
|
507
|
+
contentTypes: uniqueStrings(extracted.contentTypes),
|
|
508
|
+
redacted: extracted.redactedFields.length > 0,
|
|
509
|
+
redactedFields: uniqueStrings(extracted.redactedFields).sort(),
|
|
510
|
+
riskWarnings: uniqueStrings(extracted.riskWarnings),
|
|
511
|
+
originalByteLength: byteLength,
|
|
512
|
+
previewByteLength,
|
|
513
|
+
maxPreviewBytes: MAX_PREVIEW_BYTES,
|
|
514
|
+
resultHash,
|
|
515
|
+
copyAllowed: extracted.kind !== 'binary',
|
|
516
|
+
attachAllowed: extracted.kind !== 'binary' && extracted.riskWarnings.length === 0,
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function extractPreviewText(value) {
|
|
521
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
522
|
+
const content = Array.isArray(record.content) ? record.content : undefined;
|
|
523
|
+
const contents = Array.isArray(record.contents) ? record.contents : undefined;
|
|
524
|
+
const messages = Array.isArray(record.messages) ? record.messages : undefined;
|
|
525
|
+
if (content) {
|
|
526
|
+
const parts = [];
|
|
527
|
+
let hasBinary = false;
|
|
528
|
+
const contentTypes = [];
|
|
529
|
+
const redactedFields = [];
|
|
530
|
+
const riskWarnings = [];
|
|
531
|
+
for (const item of content) {
|
|
532
|
+
const entry = item && typeof item === 'object' ? item : {};
|
|
533
|
+
if (typeof entry.mimeType === 'string')
|
|
534
|
+
contentTypes.push(entry.mimeType);
|
|
535
|
+
if (typeof entry.type === 'string')
|
|
536
|
+
contentTypes.push(entry.type);
|
|
537
|
+
if (typeof entry.text === 'string') {
|
|
538
|
+
const redacted = redactPreviewText(entry.text, 'content.text');
|
|
539
|
+
parts.push(redacted.text);
|
|
540
|
+
redactedFields.push(...redacted.fields);
|
|
541
|
+
riskWarnings.push(...redacted.warnings);
|
|
542
|
+
}
|
|
543
|
+
else if (typeof entry.uri === 'string')
|
|
544
|
+
parts.push(entry.uri);
|
|
545
|
+
else {
|
|
546
|
+
hasBinary = true;
|
|
547
|
+
const redacted = redactPreviewValue(entry);
|
|
548
|
+
parts.push(JSON.stringify(redacted.value).slice(0, 1000));
|
|
549
|
+
redactedFields.push(...redacted.fields);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
kind: hasBinary && parts.length > 0 ? 'mixed' : hasBinary ? 'binary' : parts.length > 0 ? 'text' : 'empty',
|
|
554
|
+
text: parts.join('\n\n'),
|
|
555
|
+
itemCount: content.length,
|
|
556
|
+
contentTypes: contentTypes.length ? contentTypes : ['text/plain'],
|
|
557
|
+
redactedFields,
|
|
558
|
+
riskWarnings,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
if (contents) {
|
|
562
|
+
const parts = [];
|
|
563
|
+
let hasBinary = false;
|
|
564
|
+
const contentTypes = [];
|
|
565
|
+
const redactedFields = [];
|
|
566
|
+
const riskWarnings = [];
|
|
567
|
+
for (const item of contents) {
|
|
568
|
+
const entry = item && typeof item === 'object' ? item : {};
|
|
569
|
+
if (typeof entry.mimeType === 'string')
|
|
570
|
+
contentTypes.push(entry.mimeType);
|
|
571
|
+
if (typeof entry.text === 'string') {
|
|
572
|
+
const redacted = redactPreviewText(entry.text, 'contents.text');
|
|
573
|
+
parts.push(redacted.text);
|
|
574
|
+
redactedFields.push(...redacted.fields);
|
|
575
|
+
riskWarnings.push(...redacted.warnings);
|
|
576
|
+
}
|
|
577
|
+
else if (typeof entry.blob === 'string') {
|
|
578
|
+
hasBinary = true;
|
|
579
|
+
contentTypes.push('application/octet-stream');
|
|
580
|
+
parts.push(`[binary blob: ${Buffer.byteLength(entry.blob, 'utf-8')} encoded bytes]`);
|
|
581
|
+
}
|
|
582
|
+
else if (typeof entry.uri === 'string') {
|
|
583
|
+
parts.push(entry.uri);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
kind: hasBinary && parts.length > 0 ? 'mixed' : hasBinary ? 'binary' : parts.length > 0 ? 'text' : 'empty',
|
|
588
|
+
text: parts.join('\n\n'),
|
|
589
|
+
itemCount: contents.length,
|
|
590
|
+
contentTypes: contentTypes.length ? contentTypes : ['text/plain'],
|
|
591
|
+
redactedFields,
|
|
592
|
+
riskWarnings,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (messages) {
|
|
596
|
+
const parts = [];
|
|
597
|
+
const contentTypes = [];
|
|
598
|
+
const redactedFields = [];
|
|
599
|
+
const riskWarnings = [];
|
|
600
|
+
for (const item of messages) {
|
|
601
|
+
const entry = item && typeof item === 'object' ? item : {};
|
|
602
|
+
const role = typeof entry.role === 'string' ? entry.role : 'message';
|
|
603
|
+
if (typeof entry.content === 'string') {
|
|
604
|
+
const redacted = redactPreviewText(entry.content, `messages.${role}`);
|
|
605
|
+
parts.push(`${role}: ${redacted.text}`);
|
|
606
|
+
redactedFields.push(...redacted.fields);
|
|
607
|
+
riskWarnings.push(...redacted.warnings);
|
|
608
|
+
contentTypes.push('text/plain');
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const redacted = redactPreviewValue(entry.content);
|
|
612
|
+
parts.push(`${role}: ${JSON.stringify(redacted.value)}`);
|
|
613
|
+
redactedFields.push(...redacted.fields.map((field) => `messages.${role}.${field}`));
|
|
614
|
+
contentTypes.push('application/json');
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
kind: parts.length > 0 ? 'text' : 'empty',
|
|
619
|
+
text: parts.join('\n\n'),
|
|
620
|
+
itemCount: messages.length,
|
|
621
|
+
contentTypes,
|
|
622
|
+
redactedFields,
|
|
623
|
+
riskWarnings,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const redacted = redactPreviewValue(value);
|
|
627
|
+
const json = JSON.stringify(redacted.value, null, 2);
|
|
628
|
+
return {
|
|
629
|
+
kind: json && json !== 'undefined' ? 'json' : 'empty',
|
|
630
|
+
text: json && json !== 'undefined' ? json : '',
|
|
631
|
+
itemCount: Array.isArray(value) ? value.length : value === undefined || value === null ? 0 : 1,
|
|
632
|
+
contentTypes: json && json !== 'undefined' ? ['application/json'] : [],
|
|
633
|
+
redactedFields: redacted.fields,
|
|
634
|
+
riskWarnings: [],
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function redactPreviewText(value, field) {
|
|
638
|
+
const fields = [];
|
|
639
|
+
const warnings = [];
|
|
640
|
+
let text = value;
|
|
641
|
+
const patterns = [
|
|
642
|
+
[/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{16,}/gi, '$1[redacted]'],
|
|
643
|
+
[/\b(sk-[A-Za-z0-9_-]{16,})\b/g, '[redacted]'],
|
|
644
|
+
[/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTHORIZATION)[A-Z0-9_]*\s*[:=]\s*)(["']?)[^\s"',;]{6,}\2/gi, '$1[redacted]'],
|
|
645
|
+
];
|
|
646
|
+
for (const [pattern, replacement] of patterns) {
|
|
647
|
+
if (pattern.test(text)) {
|
|
648
|
+
fields.push(field);
|
|
649
|
+
text = text.replace(pattern, replacement);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (/\b(ignore (all )?(previous|prior) instructions|system prompt|developer message|exfiltrate|send (the )?secrets?)\b/i.test(value)) {
|
|
653
|
+
warnings.push('MCP output contains prompt-injection language. Review before attaching to an agent context.');
|
|
654
|
+
}
|
|
655
|
+
return { text, fields, warnings };
|
|
656
|
+
}
|
|
657
|
+
function redactPreviewValue(value, path = '') {
|
|
658
|
+
if (Array.isArray(value)) {
|
|
659
|
+
const fields = [];
|
|
660
|
+
const entries = value.map((entry, index) => {
|
|
661
|
+
const redacted = redactPreviewValue(entry, `${path}[${index}]`);
|
|
662
|
+
fields.push(...redacted.fields);
|
|
663
|
+
return redacted.value;
|
|
664
|
+
});
|
|
665
|
+
return { value: entries, fields };
|
|
666
|
+
}
|
|
667
|
+
if (!value || typeof value !== 'object')
|
|
668
|
+
return { value, fields: [] };
|
|
669
|
+
const out = {};
|
|
670
|
+
const fields = [];
|
|
671
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
672
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
673
|
+
if (/token|secret|password|cookie|authorization|api[-_]?key/i.test(key)) {
|
|
674
|
+
out[key] = '[redacted]';
|
|
675
|
+
fields.push(childPath);
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
const redacted = redactPreviewValue(entry, childPath);
|
|
679
|
+
out[key] = redacted.value;
|
|
680
|
+
fields.push(...redacted.fields);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return { value: out, fields };
|
|
684
|
+
}
|
|
685
|
+
export class PluginMcpManagerService {
|
|
686
|
+
baseDir;
|
|
687
|
+
pluginDir;
|
|
688
|
+
pluginEventLogPath;
|
|
689
|
+
pluginTrustPath;
|
|
690
|
+
mcpDir;
|
|
691
|
+
mcpServersPath;
|
|
692
|
+
mcpApprovalsPath;
|
|
693
|
+
pluginHost = null;
|
|
694
|
+
mcpManager = null;
|
|
695
|
+
mcpUnsubscribe = null;
|
|
696
|
+
pluginEvents = [];
|
|
697
|
+
activePluginsHydrated = false;
|
|
698
|
+
constructor(baseDir = join(homedir(), '.xeno')) {
|
|
699
|
+
this.baseDir = baseDir;
|
|
700
|
+
this.pluginDir = join(baseDir, 'plugins');
|
|
701
|
+
this.pluginEventLogPath = join(this.pluginDir, 'events.jsonl');
|
|
702
|
+
this.pluginTrustPath = join(this.pluginDir, 'trust.json');
|
|
703
|
+
this.mcpDir = join(baseDir, 'mcp');
|
|
704
|
+
this.mcpServersPath = join(this.mcpDir, 'servers.json');
|
|
705
|
+
this.mcpApprovalsPath = join(this.mcpDir, 'approvals.jsonl');
|
|
706
|
+
ensureDir(this.pluginDir);
|
|
707
|
+
ensureDir(this.mcpDir);
|
|
708
|
+
this.pluginEvents = this.loadPluginEvents();
|
|
709
|
+
}
|
|
710
|
+
async snapshot(request = {}) {
|
|
711
|
+
const host = await this.getPluginHost();
|
|
712
|
+
const manager = await this.getMcpManager();
|
|
713
|
+
const file = this.readMcpFile();
|
|
714
|
+
const trustFile = this.readPluginTrustFile();
|
|
715
|
+
const caller = normalizeCaller(request);
|
|
716
|
+
const effectivePolicy = this.resolveEffectivePolicy(caller, file);
|
|
717
|
+
const installed = host.listInstalled();
|
|
718
|
+
const plugins = installed.map((plugin) => sanitizePluginInfo(plugin, trustFile));
|
|
719
|
+
const activePluginTools = installed
|
|
720
|
+
.filter((plugin) => {
|
|
721
|
+
const trust = analyzePluginTrust(plugin, trustFile);
|
|
722
|
+
return plugin.status === 'active' && trust.state === 'trusted' && this.isPluginAllowed(plugin.manifest.name, effectivePolicy);
|
|
723
|
+
})
|
|
724
|
+
.flatMap((plugin) => [
|
|
725
|
+
...stringArray(plugin.manifest.tools),
|
|
726
|
+
...stringArray(plugin.manifest.contributes?.tools?.map((tool) => tool.name)),
|
|
727
|
+
...stringArray(plugin.registeredTools?.map(pluginToolName)),
|
|
728
|
+
]);
|
|
729
|
+
const configured = await this.listConfiguredMcpRecords(request);
|
|
730
|
+
const runtimeStates = manager.listServers();
|
|
731
|
+
const mcpServers = this.mergeMcpServerSummaries(configured, runtimeStates, manager);
|
|
732
|
+
const mcpTools = manager.listTools().map(summarizeMcpTool);
|
|
733
|
+
const mcpPrompts = manager.listPrompts().map(summarizeMcpPrompt);
|
|
734
|
+
const mcpResources = manager.listResources().map(summarizeMcpResource);
|
|
735
|
+
const activePluginCount = plugins.filter((plugin) => plugin.status === 'active').length;
|
|
736
|
+
return {
|
|
737
|
+
plugins,
|
|
738
|
+
pluginEvents: this.pluginEvents.slice(-MAX_PLUGIN_EVENTS),
|
|
739
|
+
activePluginTools,
|
|
740
|
+
mcpServers,
|
|
741
|
+
mcpTools,
|
|
742
|
+
mcpPrompts,
|
|
743
|
+
mcpResources,
|
|
744
|
+
mcpRegistryPolicy: file.registryPolicy,
|
|
745
|
+
scopedPolicies: file.scopedPolicies,
|
|
746
|
+
effectivePolicy,
|
|
747
|
+
stats: {
|
|
748
|
+
pluginCount: plugins.length,
|
|
749
|
+
activePluginCount,
|
|
750
|
+
activePluginToolCount: activePluginTools.length,
|
|
751
|
+
mcpServerCount: mcpServers.length,
|
|
752
|
+
connectedMcpServerCount: mcpServers.filter((server) => server.status === 'ready').length,
|
|
753
|
+
mcpToolCount: mcpTools.length,
|
|
754
|
+
mcpPromptCount: mcpPrompts.length,
|
|
755
|
+
mcpResourceCount: mcpResources.length,
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
async installPlugin(request) {
|
|
760
|
+
try {
|
|
761
|
+
const packageName = normalizeSearchText(request.packageName);
|
|
762
|
+
if (!packageName || !isNpmPackageName(packageName)) {
|
|
763
|
+
return { success: false, error: 'Invalid npm package name.' };
|
|
764
|
+
}
|
|
765
|
+
const host = await this.getPluginHost();
|
|
766
|
+
const info = await host.install(packageName, optionalString(request.version));
|
|
767
|
+
return { success: info.status !== 'error', plugin: sanitizePluginInfo(info, this.readPluginTrustFile()), error: info.error };
|
|
768
|
+
}
|
|
769
|
+
catch (err) {
|
|
770
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async installLocalPlugin(request) {
|
|
774
|
+
try {
|
|
775
|
+
const pluginPath = optionalString(request.pluginPath);
|
|
776
|
+
if (!pluginPath)
|
|
777
|
+
return { success: false, error: 'Plugin path is required.' };
|
|
778
|
+
const allowed = stringArray(request.caller?.allowedDirectories);
|
|
779
|
+
if (allowed.length > 0 && !isPathWithinAllowed(pluginPath, allowed)) {
|
|
780
|
+
return { success: false, error: 'Plugin path is outside the caller allowed directories.' };
|
|
781
|
+
}
|
|
782
|
+
const host = await this.getPluginHost();
|
|
783
|
+
const info = await host.installLocal(resolve(pluginPath));
|
|
784
|
+
return { success: true, plugin: sanitizePluginInfo(info, this.readPluginTrustFile()) };
|
|
785
|
+
}
|
|
786
|
+
catch (err) {
|
|
787
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
async activatePlugin(request) {
|
|
791
|
+
return this.withPlugin(request.name, async (host, name) => {
|
|
792
|
+
const before = host.getPlugin(name);
|
|
793
|
+
if (!before)
|
|
794
|
+
return { success: false, error: `Plugin "${name}" is not installed.` };
|
|
795
|
+
const trust = analyzePluginTrust(before, this.readPluginTrustFile());
|
|
796
|
+
if (trust.state !== 'trusted') {
|
|
797
|
+
return { success: false, plugin: sanitizePluginInfo(before, this.readPluginTrustFile()), error: `Plugin "${name}" requires trust approval before activation: ${trust.reasons.join(' ')}` };
|
|
798
|
+
}
|
|
799
|
+
await host.activate(name);
|
|
800
|
+
const plugin = host.getPlugin(name);
|
|
801
|
+
return { success: !!plugin && plugin.status !== 'error', plugin: plugin ? sanitizePluginInfo(plugin, this.readPluginTrustFile()) : undefined, error: plugin?.error };
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
async deactivatePlugin(request) {
|
|
805
|
+
return this.withPlugin(request.name, async (host, name) => {
|
|
806
|
+
await host.deactivate(name);
|
|
807
|
+
const plugin = host.getPlugin(name);
|
|
808
|
+
return { success: true, plugin: plugin ? sanitizePluginInfo(plugin, this.readPluginTrustFile()) : undefined };
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
async uninstallPlugin(request) {
|
|
812
|
+
try {
|
|
813
|
+
const name = normalizeSearchText(request.name);
|
|
814
|
+
if (!name)
|
|
815
|
+
return { success: false, error: 'Plugin name is required.' };
|
|
816
|
+
const host = await this.getPluginHost();
|
|
817
|
+
const removed = await host.uninstall(name, request.removeFiles ?? false);
|
|
818
|
+
return removed ? { success: true } : { success: false, error: `Plugin "${name}" is not installed.` };
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async updatePlugin(request) {
|
|
825
|
+
return this.withPlugin(request.name, async (host, name) => {
|
|
826
|
+
const info = await host.update(name, optionalString(request.version));
|
|
827
|
+
return { success: info.status !== 'error', plugin: sanitizePluginInfo(info, this.readPluginTrustFile()), error: info.error };
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
async setPluginTrust(request) {
|
|
831
|
+
return this.withPlugin(request.name, async (host, name) => {
|
|
832
|
+
const plugin = host.getPlugin(name);
|
|
833
|
+
if (!plugin)
|
|
834
|
+
return { success: false, error: `Plugin "${name}" is not installed.` };
|
|
835
|
+
const file = this.readPluginTrustFile();
|
|
836
|
+
const key = normalizePolicyKey(name);
|
|
837
|
+
if (request.decision === 'cleared') {
|
|
838
|
+
delete file.plugins[key];
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
file.plugins[key] = {
|
|
842
|
+
decision: request.decision === 'approved' ? 'approved' : 'denied',
|
|
843
|
+
manifestHash: hashJson(plugin.manifest),
|
|
844
|
+
integrityHash: plugin.integrityHash,
|
|
845
|
+
updatedAt: Date.now(),
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
this.writePluginTrustFile(file);
|
|
849
|
+
if (request.decision === 'denied' && plugin.status === 'active') {
|
|
850
|
+
await host.deactivate(name);
|
|
851
|
+
}
|
|
852
|
+
this.appendMcpApproval({
|
|
853
|
+
type: 'plugin-trust.decision',
|
|
854
|
+
targetType: 'plugin',
|
|
855
|
+
name,
|
|
856
|
+
decision: request.decision,
|
|
857
|
+
manifestHash: hashJson(plugin.manifest),
|
|
858
|
+
integrityHash: plugin.integrityHash,
|
|
859
|
+
riskLevel: analyzePluginTrust(plugin, file).riskLevel,
|
|
860
|
+
actor: normalizeCaller(request.caller),
|
|
861
|
+
reason: optionalString(request.reason),
|
|
862
|
+
timestamp: new Date().toISOString(),
|
|
863
|
+
});
|
|
864
|
+
const current = host.getPlugin(name) || plugin;
|
|
865
|
+
return { success: true, plugin: sanitizePluginInfo(current, file) };
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
async upsertMcpServer(request) {
|
|
869
|
+
try {
|
|
870
|
+
const server = this.normalizeHubMcpServer(request.server);
|
|
871
|
+
const file = this.readMcpFile();
|
|
872
|
+
const existing = file.servers.find((entry) => entry.name === server.name);
|
|
873
|
+
const next = existing
|
|
874
|
+
? {
|
|
875
|
+
...existing,
|
|
876
|
+
...server,
|
|
877
|
+
approvalState: existing.approvalState,
|
|
878
|
+
approvalConfigHash: existing.approvalConfigHash,
|
|
879
|
+
createdAt: existing.createdAt,
|
|
880
|
+
updatedAt: Date.now(),
|
|
881
|
+
}
|
|
882
|
+
: server;
|
|
883
|
+
if (existing && mcpConfigHash(existing) !== mcpConfigHash(next)) {
|
|
884
|
+
next.approvalState = undefined;
|
|
885
|
+
next.approvalConfigHash = undefined;
|
|
886
|
+
const manager = await this.getMcpManager();
|
|
887
|
+
manager.removeServer(next.name);
|
|
888
|
+
}
|
|
889
|
+
file.servers = [
|
|
890
|
+
...file.servers.filter((entry) => entry.name !== server.name),
|
|
891
|
+
next,
|
|
892
|
+
].sort((left, right) => left.name.localeCompare(right.name));
|
|
893
|
+
this.writeMcpFile(file);
|
|
894
|
+
return { success: true, server: next };
|
|
895
|
+
}
|
|
896
|
+
catch (err) {
|
|
897
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
async removeMcpServer(request) {
|
|
901
|
+
try {
|
|
902
|
+
const name = normalizeSearchText(request.name);
|
|
903
|
+
if (!name)
|
|
904
|
+
return { success: false, error: 'MCP server name is required.' };
|
|
905
|
+
const manager = await this.getMcpManager();
|
|
906
|
+
manager.removeServer(name);
|
|
907
|
+
const file = this.readMcpFile();
|
|
908
|
+
file.servers = file.servers.filter((entry) => entry.name !== name);
|
|
909
|
+
this.writeMcpFile(file);
|
|
910
|
+
return { success: true };
|
|
911
|
+
}
|
|
912
|
+
catch (err) {
|
|
913
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async setMcpApproval(request) {
|
|
917
|
+
try {
|
|
918
|
+
const name = normalizeSearchText(request.name);
|
|
919
|
+
if (!name)
|
|
920
|
+
return { success: false, error: 'MCP server name is required.' };
|
|
921
|
+
const file = this.readMcpFile();
|
|
922
|
+
const server = file.servers.find((entry) => entry.name === name);
|
|
923
|
+
if (!server)
|
|
924
|
+
return { success: false, error: `MCP server "${name}" is not configured in Hub.` };
|
|
925
|
+
const previousDecision = server.approvalState;
|
|
926
|
+
const previousConfigHash = server.approvalConfigHash;
|
|
927
|
+
const currentConfigHash = mcpConfigHash(server);
|
|
928
|
+
const configDrift = !!previousConfigHash && previousConfigHash !== currentConfigHash;
|
|
929
|
+
server.approvalState = request.decision;
|
|
930
|
+
if (request.decision === 'approved' || request.decision === 'denied') {
|
|
931
|
+
server.approvalConfigHash = mcpConfigHash(server);
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
server.approvalConfigHash = undefined;
|
|
935
|
+
}
|
|
936
|
+
server.updatedAt = Date.now();
|
|
937
|
+
this.writeMcpFile(file);
|
|
938
|
+
if (request.decision !== 'approved') {
|
|
939
|
+
const manager = await this.getMcpManager();
|
|
940
|
+
manager.removeServer(name);
|
|
941
|
+
}
|
|
942
|
+
this.appendMcpApproval({
|
|
943
|
+
type: 'mcp-approval.decision',
|
|
944
|
+
targetType: 'mcp',
|
|
945
|
+
name,
|
|
946
|
+
approvalKey: server.approvalKey,
|
|
947
|
+
previousDecision,
|
|
948
|
+
decision: request.decision || 'cleared',
|
|
949
|
+
previousConfigHash,
|
|
950
|
+
currentConfigHash,
|
|
951
|
+
configHash: server.approvalConfigHash,
|
|
952
|
+
configDrift,
|
|
953
|
+
transport: server.transport,
|
|
954
|
+
source: server.source,
|
|
955
|
+
sourcePath: server.sourcePath,
|
|
956
|
+
actor: normalizeCaller(request.caller),
|
|
957
|
+
reason: optionalString(request.reason),
|
|
958
|
+
timestamp: new Date().toISOString(),
|
|
959
|
+
});
|
|
960
|
+
return { success: true, server };
|
|
961
|
+
}
|
|
962
|
+
catch (err) {
|
|
963
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
async reloadMcp(request = {}) {
|
|
967
|
+
const connected = [];
|
|
968
|
+
const skipped = [];
|
|
969
|
+
const errors = [];
|
|
970
|
+
try {
|
|
971
|
+
this.disposeMcpManager();
|
|
972
|
+
const manager = await this.getMcpManager();
|
|
973
|
+
const configured = await this.listConfiguredMcpRecords({ cwd: request.cwd, includeSdkConfigured: true });
|
|
974
|
+
const file = this.readMcpFile();
|
|
975
|
+
const effectivePolicy = this.resolveEffectivePolicy(normalizeCaller(request), file);
|
|
976
|
+
if (request.connect) {
|
|
977
|
+
for (const server of configured) {
|
|
978
|
+
if (!this.isMcpServerAllowed(server.name, effectivePolicy)) {
|
|
979
|
+
skipped.push(server.name);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (server.disabled) {
|
|
983
|
+
skipped.push(server.name);
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (analyzeMcpTrust(server).state !== 'trusted') {
|
|
987
|
+
skipped.push(server.name);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
try {
|
|
991
|
+
await manager.addServer(mcpRecordToConfig(server));
|
|
992
|
+
connected.push(server.name);
|
|
993
|
+
}
|
|
994
|
+
catch (err) {
|
|
995
|
+
errors.push({ name: server.name, error: err instanceof Error ? err.message : String(err) });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
skipped.push(...configured.map((server) => server.name));
|
|
1001
|
+
}
|
|
1002
|
+
return { success: errors.length === 0, snapshot: await this.snapshot({ cwd: request.cwd, includeSdkConfigured: true, ...normalizeCaller(request) }), connected, skipped, errors };
|
|
1003
|
+
}
|
|
1004
|
+
catch (err) {
|
|
1005
|
+
return { success: false, connected, skipped, errors, error: err instanceof Error ? err.message : String(err) };
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async setMcpRegistryPolicy(request) {
|
|
1009
|
+
try {
|
|
1010
|
+
const file = this.readMcpFile();
|
|
1011
|
+
file.registryPolicy = normalizeRegistryPolicy(request.policy);
|
|
1012
|
+
this.writeMcpFile(file);
|
|
1013
|
+
return { success: true, snapshot: await this.snapshot() };
|
|
1014
|
+
}
|
|
1015
|
+
catch (err) {
|
|
1016
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async setScopedPolicy(request) {
|
|
1020
|
+
try {
|
|
1021
|
+
const file = this.readMcpFile();
|
|
1022
|
+
const scope = normalizePolicyScope(request.policy?.scope);
|
|
1023
|
+
const id = optionalString(request.policy?.id) || policyScopeId(scope);
|
|
1024
|
+
const existing = file.scopedPolicies.find((entry) => entry.id === id || policyScopeId(entry.scope) === policyScopeId(scope));
|
|
1025
|
+
const policy = normalizeScopedPolicy({ ...request.policy, id, scope }, existing);
|
|
1026
|
+
policy.updatedAt = Date.now();
|
|
1027
|
+
file.scopedPolicies = [
|
|
1028
|
+
...file.scopedPolicies.filter((entry) => entry.id !== policy.id && policyScopeId(entry.scope) !== policyScopeId(policy.scope)),
|
|
1029
|
+
policy,
|
|
1030
|
+
].sort((left, right) => scopeSpecificity(left.scope) - scopeSpecificity(right.scope) || left.id.localeCompare(right.id));
|
|
1031
|
+
this.writeMcpFile(file);
|
|
1032
|
+
return { success: true, policy, snapshot: await this.snapshot() };
|
|
1033
|
+
}
|
|
1034
|
+
catch (err) {
|
|
1035
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async deleteScopedPolicy(request) {
|
|
1039
|
+
try {
|
|
1040
|
+
const file = this.readMcpFile();
|
|
1041
|
+
const id = optionalString(request.id);
|
|
1042
|
+
const scopeId = request.scope ? policyScopeId(normalizePolicyScope(request.scope)) : undefined;
|
|
1043
|
+
if (!id && !scopeId)
|
|
1044
|
+
return { success: false, error: 'Policy id or scope is required.' };
|
|
1045
|
+
const before = file.scopedPolicies.length;
|
|
1046
|
+
file.scopedPolicies = file.scopedPolicies.filter((policy) => policy.id !== id && policyScopeId(policy.scope) !== scopeId);
|
|
1047
|
+
if (file.scopedPolicies.length === before)
|
|
1048
|
+
return { success: false, error: 'Scoped policy was not found.' };
|
|
1049
|
+
this.writeMcpFile(file);
|
|
1050
|
+
return { success: true, snapshot: await this.snapshot() };
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
async searchMarketplace(request = {}) {
|
|
1057
|
+
try {
|
|
1058
|
+
const sdk = await loadSdk();
|
|
1059
|
+
const registry = optionalString(request.registry);
|
|
1060
|
+
const marketplace = new sdk.PluginMarketplace({
|
|
1061
|
+
timeout: 8000,
|
|
1062
|
+
...(registry !== undefined ? { registry } : {}),
|
|
1063
|
+
});
|
|
1064
|
+
const query = optionalString(request.query);
|
|
1065
|
+
const keyword = optionalString(request.keyword);
|
|
1066
|
+
const listings = await marketplace.search({
|
|
1067
|
+
limit: typeof request.limit === 'number' ? Math.max(1, Math.min(request.limit, 50)) : 20,
|
|
1068
|
+
offset: typeof request.offset === 'number' ? Math.max(0, request.offset) : 0,
|
|
1069
|
+
verifiedOnly: request.verifiedOnly === true,
|
|
1070
|
+
...(query !== undefined ? { query } : {}),
|
|
1071
|
+
...(keyword !== undefined ? { keyword } : {}),
|
|
1072
|
+
...(request.capability !== undefined ? { capability: request.capability } : {}),
|
|
1073
|
+
...(request.sortBy !== undefined ? { sortBy: request.sortBy } : {}),
|
|
1074
|
+
});
|
|
1075
|
+
return { success: true, listings };
|
|
1076
|
+
}
|
|
1077
|
+
catch (err) {
|
|
1078
|
+
return { success: false, listings: [], error: err instanceof Error ? err.message : String(err) };
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async syncProjectMcp(request) {
|
|
1082
|
+
const direction = request.direction === 'export' ? 'export' : 'import';
|
|
1083
|
+
try {
|
|
1084
|
+
const cwd = optionalString(request.cwd);
|
|
1085
|
+
if (!cwd)
|
|
1086
|
+
return { success: false, direction, synced: 0, servers: [], error: 'Project cwd is required.' };
|
|
1087
|
+
const caller = normalizeCaller(request.caller);
|
|
1088
|
+
if (caller.allowedDirectories && caller.allowedDirectories.length > 0 && !isPathWithinAllowed(cwd, caller.allowedDirectories)) {
|
|
1089
|
+
return { success: false, direction, synced: 0, servers: [], error: 'Project cwd is outside the caller allowed directories.' };
|
|
1090
|
+
}
|
|
1091
|
+
const projectCwd = resolve(cwd);
|
|
1092
|
+
const sdk = await loadSdk();
|
|
1093
|
+
const configPath = join(projectCwd, '.xeno', 'mcp.json');
|
|
1094
|
+
const selectedNames = new Set(stringArray(request.serverNames).map((name) => normalizePolicyKey(name)));
|
|
1095
|
+
if (direction === 'import') {
|
|
1096
|
+
const config = sdk.loadMcpConfigFile(configPath, 'project');
|
|
1097
|
+
const imported = Object.entries(config.mcpServers || {})
|
|
1098
|
+
.map(([name, server]) => mcpRecordFromConfig({
|
|
1099
|
+
...server,
|
|
1100
|
+
name,
|
|
1101
|
+
scope: 'project',
|
|
1102
|
+
sourcePath: configPath,
|
|
1103
|
+
approvalKey: `project:${name}`,
|
|
1104
|
+
}, configPath, 'hub'))
|
|
1105
|
+
.filter((server) => selectedNames.size === 0 || selectedNames.has(normalizePolicyKey(server.name)));
|
|
1106
|
+
const file = this.readMcpFile();
|
|
1107
|
+
const existingByName = new Map(file.servers.map((server) => [server.name, server]));
|
|
1108
|
+
for (const server of imported) {
|
|
1109
|
+
const existing = existingByName.get(server.name);
|
|
1110
|
+
const next = {
|
|
1111
|
+
...(existing || server),
|
|
1112
|
+
...server,
|
|
1113
|
+
source: 'hub',
|
|
1114
|
+
createdAt: existing?.createdAt || server.createdAt,
|
|
1115
|
+
updatedAt: Date.now(),
|
|
1116
|
+
};
|
|
1117
|
+
if (existing && mcpConfigHash(existing) !== mcpConfigHash(next)) {
|
|
1118
|
+
next.approvalState = undefined;
|
|
1119
|
+
next.approvalConfigHash = undefined;
|
|
1120
|
+
const manager = await this.getMcpManager();
|
|
1121
|
+
manager.removeServer(next.name);
|
|
1122
|
+
}
|
|
1123
|
+
existingByName.set(server.name, next);
|
|
1124
|
+
}
|
|
1125
|
+
file.servers = Array.from(existingByName.values());
|
|
1126
|
+
this.writeMcpFile(file);
|
|
1127
|
+
return {
|
|
1128
|
+
success: true,
|
|
1129
|
+
direction,
|
|
1130
|
+
configPath,
|
|
1131
|
+
synced: imported.length,
|
|
1132
|
+
servers: imported,
|
|
1133
|
+
snapshot: await this.snapshot({ cwd: projectCwd, includeSdkConfigured: true, ...caller }),
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
const file = this.readMcpFile();
|
|
1137
|
+
const effective = this.resolveEffectivePolicy(caller, file);
|
|
1138
|
+
const exported = file.servers
|
|
1139
|
+
.filter((server) => selectedNames.size === 0 || selectedNames.has(normalizePolicyKey(server.name)))
|
|
1140
|
+
.filter((server) => this.isMcpServerAllowed(server.name, effective));
|
|
1141
|
+
const written = [];
|
|
1142
|
+
for (const server of exported) {
|
|
1143
|
+
const { sourcePath: _sourcePath, ...exportableConfig } = mcpRecordToConfig(server);
|
|
1144
|
+
const config = {
|
|
1145
|
+
...exportableConfig,
|
|
1146
|
+
scope: 'project',
|
|
1147
|
+
approvalKey: `project:${server.name}`,
|
|
1148
|
+
};
|
|
1149
|
+
sdk.upsertMcpServerConfig('project', projectCwd, config);
|
|
1150
|
+
written.push({ ...server, scope: 'project', sourcePath: configPath, approvalKey: `project:${server.name}`, updatedAt: Date.now() });
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
success: true,
|
|
1154
|
+
direction,
|
|
1155
|
+
configPath,
|
|
1156
|
+
synced: written.length,
|
|
1157
|
+
servers: written,
|
|
1158
|
+
snapshot: await this.snapshot({ cwd: projectCwd, includeSdkConfigured: true, ...caller }),
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
catch (err) {
|
|
1162
|
+
return { success: false, direction, synced: 0, servers: [], error: err instanceof Error ? err.message : String(err) };
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async callMcpTool(request) {
|
|
1166
|
+
try {
|
|
1167
|
+
this.assertMcpServerUsable(request.serverName, request.caller, 'tool');
|
|
1168
|
+
const manager = await this.getMcpManager();
|
|
1169
|
+
const result = await manager.callTool(request.serverName, request.toolName, request.input || {});
|
|
1170
|
+
const preview = createResultPreview(result, `${request.serverName}.${request.toolName}`, 'mcp-tool');
|
|
1171
|
+
const auditId = this.recordMcpCallAudit('tool', request.serverName, request.toolName, preview);
|
|
1172
|
+
return { success: true, result, preview, auditId };
|
|
1173
|
+
}
|
|
1174
|
+
catch (err) {
|
|
1175
|
+
if (err instanceof PluginMcpDeniedError)
|
|
1176
|
+
return { success: false, error: err.denial.message, denial: err.denial };
|
|
1177
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async getMcpPrompt(request) {
|
|
1181
|
+
try {
|
|
1182
|
+
this.assertMcpServerUsable(request.serverName, request.caller, 'prompt');
|
|
1183
|
+
const manager = await this.getMcpManager();
|
|
1184
|
+
const result = await manager.getPrompt(request.serverName, request.promptName, request.input);
|
|
1185
|
+
const preview = createResultPreview(result, `${request.serverName}.${request.promptName}`, 'mcp-prompt');
|
|
1186
|
+
const auditId = this.recordMcpCallAudit('prompt', request.serverName, request.promptName, preview);
|
|
1187
|
+
return { success: true, result, preview, auditId };
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
if (err instanceof PluginMcpDeniedError)
|
|
1191
|
+
return { success: false, error: err.denial.message, denial: err.denial };
|
|
1192
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
async readMcpResource(request) {
|
|
1196
|
+
try {
|
|
1197
|
+
this.assertMcpServerUsable(request.serverName, request.caller, 'resource');
|
|
1198
|
+
const manager = await this.getMcpManager();
|
|
1199
|
+
const result = await manager.readResource(request.serverName, request.uri);
|
|
1200
|
+
const preview = createResultPreview(result, `${request.serverName}:${request.uri}`, 'mcp-resource');
|
|
1201
|
+
const auditId = this.recordMcpCallAudit('resource', request.serverName, request.uri, preview);
|
|
1202
|
+
return { success: true, result, preview, auditId };
|
|
1203
|
+
}
|
|
1204
|
+
catch (err) {
|
|
1205
|
+
if (err instanceof PluginMcpDeniedError)
|
|
1206
|
+
return { success: false, error: err.denial.message, denial: err.denial };
|
|
1207
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async preflightTrust(request = {}) {
|
|
1211
|
+
try {
|
|
1212
|
+
const reasons = [];
|
|
1213
|
+
const remediationItems = [];
|
|
1214
|
+
const checks = [];
|
|
1215
|
+
let allowed = true;
|
|
1216
|
+
let pluginSummary;
|
|
1217
|
+
let serverSummary;
|
|
1218
|
+
const pluginName = optionalString(request.pluginName);
|
|
1219
|
+
if (pluginName) {
|
|
1220
|
+
let policyAllowed = true;
|
|
1221
|
+
const checkReasons = [];
|
|
1222
|
+
const host = await this.getPluginHost();
|
|
1223
|
+
const plugin = host.getPlugin(pluginName);
|
|
1224
|
+
if (!plugin) {
|
|
1225
|
+
allowed = false;
|
|
1226
|
+
checkReasons.push(`Plugin "${pluginName}" is not installed.`);
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
pluginSummary = sanitizePluginInfo(plugin, this.readPluginTrustFile());
|
|
1230
|
+
if (pluginSummary.trust.state !== 'trusted') {
|
|
1231
|
+
allowed = false;
|
|
1232
|
+
checkReasons.push(...pluginSummary.trust.reasons);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const file = this.readMcpFile();
|
|
1236
|
+
const effective = this.resolveEffectivePolicy(normalizeCaller(request.caller), file);
|
|
1237
|
+
if (!this.isPluginAllowed(pluginName, effective)) {
|
|
1238
|
+
allowed = false;
|
|
1239
|
+
policyAllowed = false;
|
|
1240
|
+
checkReasons.push(`Plugin "${pluginName}" is denied by scoped policy.`);
|
|
1241
|
+
}
|
|
1242
|
+
const checkRemediation = trustRemediation('plugin', checkReasons);
|
|
1243
|
+
reasons.push(...checkReasons);
|
|
1244
|
+
remediationItems.push(...checkRemediation);
|
|
1245
|
+
checks.push({
|
|
1246
|
+
targetType: 'plugin',
|
|
1247
|
+
targetName: pluginName,
|
|
1248
|
+
allowed: checkReasons.length === 0,
|
|
1249
|
+
policyAllowed,
|
|
1250
|
+
trustState: pluginSummary?.trust.state,
|
|
1251
|
+
configHash: pluginSummary?.trust.manifestHash,
|
|
1252
|
+
reasons: checkReasons,
|
|
1253
|
+
remediation: checkRemediation,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
const mcpServerName = optionalString(request.mcpServerName);
|
|
1257
|
+
if (mcpServerName) {
|
|
1258
|
+
let policyAllowed = true;
|
|
1259
|
+
const checkReasons = [];
|
|
1260
|
+
const file = this.readMcpFile();
|
|
1261
|
+
const server = file.servers.find((entry) => entry.name === mcpServerName);
|
|
1262
|
+
if (!server) {
|
|
1263
|
+
allowed = false;
|
|
1264
|
+
checkReasons.push(`MCP server "${mcpServerName}" is not configured in Hub.`);
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
const manager = await this.getMcpManager();
|
|
1268
|
+
const state = manager.listServers().find((entry) => entry.config.name === mcpServerName);
|
|
1269
|
+
serverSummary = this.mcpServerSummary(server, state, manager);
|
|
1270
|
+
const effective = this.resolveEffectivePolicy(normalizeCaller(request.caller), file);
|
|
1271
|
+
if (!this.isMcpServerAllowed(mcpServerName, effective)) {
|
|
1272
|
+
allowed = false;
|
|
1273
|
+
policyAllowed = false;
|
|
1274
|
+
checkReasons.push(`MCP server "${mcpServerName}" is denied by scoped policy.`);
|
|
1275
|
+
}
|
|
1276
|
+
if (serverSummary.trust.state !== 'trusted') {
|
|
1277
|
+
allowed = false;
|
|
1278
|
+
checkReasons.push(...serverSummary.trust.reasons);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const checkRemediation = trustRemediation('mcp', checkReasons);
|
|
1282
|
+
reasons.push(...checkReasons);
|
|
1283
|
+
remediationItems.push(...checkRemediation);
|
|
1284
|
+
checks.push({
|
|
1285
|
+
targetType: 'mcp',
|
|
1286
|
+
targetName: mcpServerName,
|
|
1287
|
+
allowed: checkReasons.length === 0,
|
|
1288
|
+
policyAllowed,
|
|
1289
|
+
trustState: serverSummary?.trust.state,
|
|
1290
|
+
configHash: serverSummary?.trust.currentConfigHash || serverSummary?.trust.manifestHash,
|
|
1291
|
+
approvedConfigHash: serverSummary?.trust.approvedConfigHash,
|
|
1292
|
+
configDrift: serverSummary?.trust.configDrift,
|
|
1293
|
+
reasons: checkReasons,
|
|
1294
|
+
remediation: checkRemediation,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
const finalReasons = reasons.length ? uniqueStrings(reasons) : ['Trust preflight passed.'];
|
|
1298
|
+
const finalRemediation = allowed ? [] : mergeRemediation(remediationItems);
|
|
1299
|
+
const target = pluginName && mcpServerName
|
|
1300
|
+
? { type: 'combined', name: `${pluginName}+${mcpServerName}` }
|
|
1301
|
+
: pluginName
|
|
1302
|
+
? { type: 'plugin', name: pluginName }
|
|
1303
|
+
: mcpServerName
|
|
1304
|
+
? { type: 'mcp', name: mcpServerName }
|
|
1305
|
+
: undefined;
|
|
1306
|
+
return {
|
|
1307
|
+
success: true,
|
|
1308
|
+
allowed,
|
|
1309
|
+
target,
|
|
1310
|
+
checks,
|
|
1311
|
+
plugin: pluginSummary,
|
|
1312
|
+
server: serverSummary,
|
|
1313
|
+
reasons: finalReasons,
|
|
1314
|
+
remediation: finalRemediation,
|
|
1315
|
+
denial: allowed ? undefined : createDenial('plugin-mcp-preflight-denied', 'Plugin/MCP trust preflight denied the request.', finalReasons, finalRemediation),
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
catch (err) {
|
|
1319
|
+
return {
|
|
1320
|
+
success: false,
|
|
1321
|
+
allowed: false,
|
|
1322
|
+
checks: [],
|
|
1323
|
+
reasons: [],
|
|
1324
|
+
remediation: [remediation('retry', 'Retry', 'Retry after Plugin/MCP state is available.')],
|
|
1325
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
async registerRuntimeTools(toolRegistry, options = {}) {
|
|
1330
|
+
const errors = [];
|
|
1331
|
+
const pluginTools = [];
|
|
1332
|
+
const mcpTools = [];
|
|
1333
|
+
const file = this.readMcpFile();
|
|
1334
|
+
const effectivePolicy = this.resolveEffectivePolicy(normalizeCaller(options), file);
|
|
1335
|
+
if (options.includePlugins !== false) {
|
|
1336
|
+
try {
|
|
1337
|
+
const host = await this.getPluginHost();
|
|
1338
|
+
const trustFile = this.readPluginTrustFile();
|
|
1339
|
+
for (const plugin of host.listInstalled()) {
|
|
1340
|
+
if (plugin.status !== 'active' || !this.isPluginAllowed(plugin.manifest.name, effectivePolicy))
|
|
1341
|
+
continue;
|
|
1342
|
+
const trust = analyzePluginTrust(plugin, trustFile);
|
|
1343
|
+
if (trust.state !== 'trusted') {
|
|
1344
|
+
try {
|
|
1345
|
+
await host.deactivate(plugin.manifest.name);
|
|
1346
|
+
}
|
|
1347
|
+
catch { /* best-effort trust quarantine */ }
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
for (const tool of plugin.registeredTools || []) {
|
|
1351
|
+
const name = pluginToolName(tool);
|
|
1352
|
+
if (!name)
|
|
1353
|
+
continue;
|
|
1354
|
+
toolRegistry.register(tool);
|
|
1355
|
+
pluginTools.push(name);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch (err) {
|
|
1360
|
+
errors.push(`PluginHost: ${err instanceof Error ? err.message : String(err)}`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (options.includeMcp !== false) {
|
|
1364
|
+
try {
|
|
1365
|
+
const sdk = await loadSdk();
|
|
1366
|
+
const manager = await this.getMcpManager();
|
|
1367
|
+
if (manager.listServers().length === 0) {
|
|
1368
|
+
const reload = await this.reloadMcp({ connect: true, cwd: options.cwd, ...normalizeCaller(options) });
|
|
1369
|
+
for (const error of reload.errors)
|
|
1370
|
+
errors.push(`MCP ${error.name}: ${error.error}`);
|
|
1371
|
+
if (reload.error)
|
|
1372
|
+
errors.push(`MCP reload: ${reload.error}`);
|
|
1373
|
+
}
|
|
1374
|
+
const registered = sdk.syncMcpToolsToRegistry(manager, toolRegistry, [], (entry) => this.isMcpServerAllowed(entry.server, effectivePolicy)
|
|
1375
|
+
&& this.isMcpServerTrusted(entry.server)
|
|
1376
|
+
&& sdk.matchesMcpRegistryEntryPolicy(entry, effectivePolicy.mcpRegistryPolicy));
|
|
1377
|
+
mcpTools.push(...registered);
|
|
1378
|
+
}
|
|
1379
|
+
catch (err) {
|
|
1380
|
+
errors.push(`MCP: ${err instanceof Error ? err.message : String(err)}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return { pluginTools, mcpTools, errors };
|
|
1384
|
+
}
|
|
1385
|
+
toSearchDocuments() {
|
|
1386
|
+
const documents = [];
|
|
1387
|
+
const pluginRegistry = safeReadJson(join(this.pluginDir, 'plugins.json'), { plugins: {} });
|
|
1388
|
+
const trustFile = this.readPluginTrustFile();
|
|
1389
|
+
for (const [key, plugin] of Object.entries(pluginRegistry.plugins || {})) {
|
|
1390
|
+
const summary = sanitizePluginInfo(plugin, trustFile);
|
|
1391
|
+
const body = [
|
|
1392
|
+
summary.description,
|
|
1393
|
+
`status ${summary.status}`,
|
|
1394
|
+
`trust ${summary.trust.state} ${summary.trust.riskLevel} ${summary.trust.signatureStatus}`,
|
|
1395
|
+
`capabilities ${summary.capabilities.join(' ')}`,
|
|
1396
|
+
`permissions ${summary.permissions.join(' ')}`,
|
|
1397
|
+
`tools ${summary.tools.join(' ')}`,
|
|
1398
|
+
`prompts ${summary.prompts.join(' ')}`,
|
|
1399
|
+
`commands ${summary.commands.join(' ')}`,
|
|
1400
|
+
summary.error ? `error ${summary.error}` : '',
|
|
1401
|
+
].filter(Boolean).join('\n');
|
|
1402
|
+
documents.push({
|
|
1403
|
+
id: stableDocumentId('plugin', key),
|
|
1404
|
+
type: 'plugin',
|
|
1405
|
+
scope: {},
|
|
1406
|
+
acl: createScopeAcl({}, 'global'),
|
|
1407
|
+
title: `Plugin ${summary.name}`,
|
|
1408
|
+
body,
|
|
1409
|
+
keywords: ['plugin', summary.name, summary.status, ...summary.capabilities, ...summary.tools],
|
|
1410
|
+
sourcePath: summary.path,
|
|
1411
|
+
timestamps: {
|
|
1412
|
+
createdAt: Date.parse(summary.installedAt) || undefined,
|
|
1413
|
+
updatedAt: Date.parse(summary.installedAt) || undefined,
|
|
1414
|
+
},
|
|
1415
|
+
metrics: { tokenEstimate: estimateTokens(body), bodyBytes: body.length },
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
for (const server of this.readMcpFile().servers) {
|
|
1419
|
+
const body = [
|
|
1420
|
+
`transport ${server.transport}`,
|
|
1421
|
+
`scope ${server.scope}`,
|
|
1422
|
+
server.command ? `command ${server.command} ${server.args.join(' ')}` : '',
|
|
1423
|
+
server.url ? `url ${server.url}` : '',
|
|
1424
|
+
`approval ${server.approvalState || 'pending'}`,
|
|
1425
|
+
server.disabled ? 'disabled' : 'enabled',
|
|
1426
|
+
].filter(Boolean).join('\n');
|
|
1427
|
+
documents.push({
|
|
1428
|
+
id: stableDocumentId('mcp_server', server.name),
|
|
1429
|
+
type: 'mcp_server',
|
|
1430
|
+
scope: {},
|
|
1431
|
+
acl: createScopeAcl({}, 'global'),
|
|
1432
|
+
title: `MCP ${server.name}`,
|
|
1433
|
+
body,
|
|
1434
|
+
keywords: ['mcp', 'server', server.name, server.transport, server.scope, server.approvalState || 'pending'],
|
|
1435
|
+
sourcePath: server.sourcePath,
|
|
1436
|
+
timestamps: { createdAt: server.createdAt, updatedAt: server.updatedAt },
|
|
1437
|
+
metrics: { tokenEstimate: estimateTokens(body), bodyBytes: body.length },
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
return documents;
|
|
1441
|
+
}
|
|
1442
|
+
resolveEffectivePolicy(caller, file = this.readMcpFile()) {
|
|
1443
|
+
const normalizedCaller = normalizeCaller(caller);
|
|
1444
|
+
const matched = file.scopedPolicies
|
|
1445
|
+
.filter((policy) => policy.enabled !== false && scopeMatchesCaller(policy.scope, normalizedCaller))
|
|
1446
|
+
.sort((left, right) => scopeSpecificity(left.scope) - scopeSpecificity(right.scope));
|
|
1447
|
+
const effective = {
|
|
1448
|
+
caller: normalizedCaller,
|
|
1449
|
+
mcpRegistryPolicy: file.registryPolicy,
|
|
1450
|
+
pluginAllowlist: [],
|
|
1451
|
+
pluginDenylist: [],
|
|
1452
|
+
mcpServerAllowlist: [],
|
|
1453
|
+
mcpServerDenylist: [],
|
|
1454
|
+
matchedPolicyIds: matched.map((policy) => policy.id),
|
|
1455
|
+
};
|
|
1456
|
+
for (const policy of matched) {
|
|
1457
|
+
if (policy.mcpRegistryPolicy)
|
|
1458
|
+
effective.mcpRegistryPolicy = policy.mcpRegistryPolicy;
|
|
1459
|
+
effective.pluginAllowlist = normalizedList([...effective.pluginAllowlist, ...policy.pluginAllowlist]);
|
|
1460
|
+
effective.pluginDenylist = normalizedList([...effective.pluginDenylist, ...policy.pluginDenylist]);
|
|
1461
|
+
effective.mcpServerAllowlist = normalizedList([...effective.mcpServerAllowlist, ...policy.mcpServerAllowlist]);
|
|
1462
|
+
effective.mcpServerDenylist = normalizedList([...effective.mcpServerDenylist, ...policy.mcpServerDenylist]);
|
|
1463
|
+
}
|
|
1464
|
+
return effective;
|
|
1465
|
+
}
|
|
1466
|
+
isPluginAllowed(name, policy) {
|
|
1467
|
+
return isAllowedByPolicyLists(name, policy.pluginAllowlist, policy.pluginDenylist);
|
|
1468
|
+
}
|
|
1469
|
+
isMcpServerAllowed(name, policy) {
|
|
1470
|
+
return isAllowedByPolicyLists(name, policy.mcpServerAllowlist, policy.mcpServerDenylist);
|
|
1471
|
+
}
|
|
1472
|
+
isMcpServerTrusted(name, file = this.readMcpFile()) {
|
|
1473
|
+
const server = file.servers.find((entry) => entry.name === name);
|
|
1474
|
+
return !!server && analyzeMcpTrust(server).state === 'trusted';
|
|
1475
|
+
}
|
|
1476
|
+
assertMcpServerUsable(serverName, caller, accessKind) {
|
|
1477
|
+
const name = normalizeSearchText(serverName);
|
|
1478
|
+
if (!name) {
|
|
1479
|
+
throw new PluginMcpDeniedError(createDenial('mcp-name-required', 'MCP server name is required.', ['MCP server name is required.'], [remediation('configure-mcp', 'Choose MCP server', 'Select a configured MCP server before calling a tool, prompt, or resource.')]));
|
|
1480
|
+
}
|
|
1481
|
+
const file = this.readMcpFile();
|
|
1482
|
+
const server = file.servers.find((entry) => entry.name === name);
|
|
1483
|
+
if (!server) {
|
|
1484
|
+
const reasons = [`MCP server "${name}" is not configured in Hub.`];
|
|
1485
|
+
throw new PluginMcpDeniedError(createDenial('mcp-not-configured', reasons[0], reasons, trustRemediation('mcp', reasons)));
|
|
1486
|
+
}
|
|
1487
|
+
const effective = this.resolveEffectivePolicy(normalizeCaller(caller), file);
|
|
1488
|
+
if (!this.isMcpServerAllowed(name, effective)) {
|
|
1489
|
+
const reasons = [`MCP server "${name}" is denied by scoped policy.`];
|
|
1490
|
+
throw new PluginMcpDeniedError(createDenial('mcp-scoped-policy-denied', reasons[0], reasons, trustRemediation('mcp', reasons)));
|
|
1491
|
+
}
|
|
1492
|
+
const trust = analyzeMcpTrust(server);
|
|
1493
|
+
if (trust.state !== 'trusted') {
|
|
1494
|
+
const reasons = trust.reasons;
|
|
1495
|
+
throw new PluginMcpDeniedError(createDenial('mcp-not-trusted', `MCP server "${name}" is not trusted.`, reasons, trustRemediation('mcp', reasons)));
|
|
1496
|
+
}
|
|
1497
|
+
if (accessKind === 'prompt' && effective.mcpRegistryPolicy !== 'prompts' && effective.mcpRegistryPolicy !== 'context' && effective.mcpRegistryPolicy !== 'all') {
|
|
1498
|
+
const reasons = [`MCP prompts are blocked by registry policy "${effective.mcpRegistryPolicy}".`];
|
|
1499
|
+
throw new PluginMcpDeniedError(createDenial('mcp-registry-policy-denied', reasons[0], reasons, trustRemediation('mcp', reasons)));
|
|
1500
|
+
}
|
|
1501
|
+
if (accessKind === 'resource' && effective.mcpRegistryPolicy !== 'resources' && effective.mcpRegistryPolicy !== 'context' && effective.mcpRegistryPolicy !== 'all') {
|
|
1502
|
+
const reasons = [`MCP resources are blocked by registry policy "${effective.mcpRegistryPolicy}".`];
|
|
1503
|
+
throw new PluginMcpDeniedError(createDenial('mcp-registry-policy-denied', reasons[0], reasons, trustRemediation('mcp', reasons)));
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
recordMcpCallAudit(kind, serverName, target, preview) {
|
|
1507
|
+
const auditId = stableDocumentId('mcp_call', kind, serverName, target, String(Date.now()), String(Math.random()));
|
|
1508
|
+
this.appendMcpApproval({
|
|
1509
|
+
id: auditId,
|
|
1510
|
+
type: `call:${kind}`,
|
|
1511
|
+
name: serverName,
|
|
1512
|
+
target,
|
|
1513
|
+
byteLength: preview.byteLength,
|
|
1514
|
+
itemCount: preview.itemCount,
|
|
1515
|
+
truncated: preview.truncated,
|
|
1516
|
+
timestamp: new Date().toISOString(),
|
|
1517
|
+
});
|
|
1518
|
+
return auditId;
|
|
1519
|
+
}
|
|
1520
|
+
async getPluginHost() {
|
|
1521
|
+
if (this.pluginHost)
|
|
1522
|
+
return this.pluginHost;
|
|
1523
|
+
const sdk = await loadSdk();
|
|
1524
|
+
this.pluginHost = new sdk.PluginHost({
|
|
1525
|
+
pluginDir: this.pluginDir,
|
|
1526
|
+
autoActivate: false,
|
|
1527
|
+
host: {
|
|
1528
|
+
appName: 'xeno-hub',
|
|
1529
|
+
appVersion: '0.2.3',
|
|
1530
|
+
platform: process.platform,
|
|
1531
|
+
},
|
|
1532
|
+
defaultPermissions: ['tools:register', 'storage:read', 'storage:write'],
|
|
1533
|
+
sandbox: {
|
|
1534
|
+
enabled: true,
|
|
1535
|
+
executionTimeout: 30_000,
|
|
1536
|
+
blockedPaths: [join(homedir(), '.ssh'), join(homedir(), '.xeno', 'credentials.json')],
|
|
1537
|
+
},
|
|
1538
|
+
});
|
|
1539
|
+
this.pluginHost.events.onAny((event) => this.recordPluginEvent(event));
|
|
1540
|
+
await this.hydratePersistedActivePlugins(this.pluginHost);
|
|
1541
|
+
return this.pluginHost;
|
|
1542
|
+
}
|
|
1543
|
+
async hydratePersistedActivePlugins(host) {
|
|
1544
|
+
if (this.activePluginsHydrated)
|
|
1545
|
+
return;
|
|
1546
|
+
this.activePluginsHydrated = true;
|
|
1547
|
+
const active = host.listInstalled().filter((plugin) => plugin.status === 'active');
|
|
1548
|
+
const trustFile = this.readPluginTrustFile();
|
|
1549
|
+
for (const plugin of active) {
|
|
1550
|
+
try {
|
|
1551
|
+
const trust = analyzePluginTrust(plugin, trustFile);
|
|
1552
|
+
if (trust.state !== 'trusted') {
|
|
1553
|
+
await host.deactivate(plugin.manifest.name);
|
|
1554
|
+
this.recordPluginEvent({
|
|
1555
|
+
type: 'plugin:error',
|
|
1556
|
+
pluginName: plugin.manifest.name,
|
|
1557
|
+
timestamp: new Date().toISOString(),
|
|
1558
|
+
data: {
|
|
1559
|
+
phase: 'hydrate-active-plugin',
|
|
1560
|
+
reason: trust.reasons.join(' '),
|
|
1561
|
+
},
|
|
1562
|
+
});
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
host.setStatus(plugin.manifest.name, 'installed');
|
|
1566
|
+
await host.activate(plugin.manifest.name);
|
|
1567
|
+
}
|
|
1568
|
+
catch (err) {
|
|
1569
|
+
this.recordPluginEvent({
|
|
1570
|
+
type: 'plugin:error',
|
|
1571
|
+
pluginName: plugin.manifest.name,
|
|
1572
|
+
timestamp: new Date().toISOString(),
|
|
1573
|
+
data: {
|
|
1574
|
+
phase: 'hydrate-active-plugin',
|
|
1575
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1576
|
+
},
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async getMcpManager() {
|
|
1582
|
+
if (this.mcpManager)
|
|
1583
|
+
return this.mcpManager;
|
|
1584
|
+
const sdk = await loadSdk();
|
|
1585
|
+
this.mcpManager = new sdk.MCPManager();
|
|
1586
|
+
this.mcpUnsubscribe = this.mcpManager.onServerStateChanged((event) => {
|
|
1587
|
+
this.appendMcpApproval({
|
|
1588
|
+
name: event.serverName,
|
|
1589
|
+
approvalKey: event.state.config.approvalKey || event.serverName,
|
|
1590
|
+
decision: `state:${event.reason}`,
|
|
1591
|
+
status: event.state.status,
|
|
1592
|
+
error: event.state.error,
|
|
1593
|
+
timestamp: new Date().toISOString(),
|
|
1594
|
+
});
|
|
1595
|
+
});
|
|
1596
|
+
return this.mcpManager;
|
|
1597
|
+
}
|
|
1598
|
+
disposeMcpManager() {
|
|
1599
|
+
try {
|
|
1600
|
+
this.mcpUnsubscribe?.();
|
|
1601
|
+
}
|
|
1602
|
+
catch { /* noop */ }
|
|
1603
|
+
this.mcpUnsubscribe = null;
|
|
1604
|
+
try {
|
|
1605
|
+
this.mcpManager?.dispose();
|
|
1606
|
+
}
|
|
1607
|
+
catch { /* noop */ }
|
|
1608
|
+
this.mcpManager = null;
|
|
1609
|
+
}
|
|
1610
|
+
async withPlugin(inputName, handler) {
|
|
1611
|
+
try {
|
|
1612
|
+
const name = normalizeSearchText(inputName);
|
|
1613
|
+
if (!name)
|
|
1614
|
+
return { success: false, error: 'Plugin name is required.' };
|
|
1615
|
+
const host = await this.getPluginHost();
|
|
1616
|
+
if (!host.getPlugin(name))
|
|
1617
|
+
return { success: false, error: `Plugin "${name}" is not installed.` };
|
|
1618
|
+
return await handler(host, name);
|
|
1619
|
+
}
|
|
1620
|
+
catch (err) {
|
|
1621
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
normalizeHubMcpServer(input) {
|
|
1625
|
+
const name = normalizeSearchText(input.name);
|
|
1626
|
+
if (!name || !isMcpServerName(name))
|
|
1627
|
+
throw new Error('Invalid MCP server name.');
|
|
1628
|
+
const transport = input.transport || (input.url ? 'sse' : 'stdio');
|
|
1629
|
+
if (transport !== 'stdio' && transport !== 'sse')
|
|
1630
|
+
throw new Error('MCP transport must be stdio or sse.');
|
|
1631
|
+
const command = optionalString(input.command);
|
|
1632
|
+
const url = optionalString(input.url);
|
|
1633
|
+
if (transport === 'stdio' && !command)
|
|
1634
|
+
throw new Error('stdio MCP servers require a command.');
|
|
1635
|
+
if (transport === 'sse' && !url)
|
|
1636
|
+
throw new Error('sse MCP servers require a URL.');
|
|
1637
|
+
const now = Date.now();
|
|
1638
|
+
const scope = normalizeMcpScope(input.scope);
|
|
1639
|
+
return {
|
|
1640
|
+
name,
|
|
1641
|
+
transport,
|
|
1642
|
+
command,
|
|
1643
|
+
args: stringArray(input.args),
|
|
1644
|
+
env: stringRecord(input.env),
|
|
1645
|
+
cwd: optionalString(input.cwd),
|
|
1646
|
+
url,
|
|
1647
|
+
headers: stringRecord(input.headers),
|
|
1648
|
+
approvalRequired: input.approvalRequired ?? true,
|
|
1649
|
+
disabled: !!input.disabled,
|
|
1650
|
+
scope,
|
|
1651
|
+
source: 'hub',
|
|
1652
|
+
sourcePath: this.mcpServersPath,
|
|
1653
|
+
approvalKey: input.approvalKey || `${scope}:${name}`,
|
|
1654
|
+
createdAt: numberTimestamp(input.createdAt, now),
|
|
1655
|
+
updatedAt: now,
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
readPluginTrustFile() {
|
|
1659
|
+
const value = safeReadJson(this.pluginTrustPath, {});
|
|
1660
|
+
const plugins = {};
|
|
1661
|
+
for (const [name, entry] of Object.entries(value.plugins || {})) {
|
|
1662
|
+
if (!entry || typeof entry !== 'object')
|
|
1663
|
+
continue;
|
|
1664
|
+
const record = entry;
|
|
1665
|
+
const decision = record.decision === 'approved' || record.decision === 'denied' ? record.decision : undefined;
|
|
1666
|
+
if (!decision)
|
|
1667
|
+
continue;
|
|
1668
|
+
plugins[normalizePolicyKey(name)] = {
|
|
1669
|
+
decision,
|
|
1670
|
+
manifestHash: optionalString(record.manifestHash),
|
|
1671
|
+
integrityHash: optionalString(record.integrityHash),
|
|
1672
|
+
updatedAt: numberTimestamp(record.updatedAt),
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
return { version: 1, plugins };
|
|
1676
|
+
}
|
|
1677
|
+
writePluginTrustFile(file) {
|
|
1678
|
+
safeWriteJson(this.pluginTrustPath, {
|
|
1679
|
+
version: 1,
|
|
1680
|
+
plugins: Object.fromEntries(Object.entries(file.plugins).sort(([left], [right]) => left.localeCompare(right))),
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
readMcpFile() {
|
|
1684
|
+
const value = safeReadJson(this.mcpServersPath, {});
|
|
1685
|
+
const servers = Array.isArray(value.servers)
|
|
1686
|
+
? value.servers
|
|
1687
|
+
.filter((server) => !!server && typeof server === 'object' && typeof server.name === 'string')
|
|
1688
|
+
.map((server) => ({
|
|
1689
|
+
...server,
|
|
1690
|
+
source: server.source || 'hub',
|
|
1691
|
+
sourcePath: server.sourcePath || this.mcpServersPath,
|
|
1692
|
+
args: stringArray(server.args),
|
|
1693
|
+
env: stringRecord(server.env),
|
|
1694
|
+
headers: stringRecord(server.headers),
|
|
1695
|
+
approvalRequired: server.approvalRequired ?? true,
|
|
1696
|
+
approvalConfigHash: optionalString(server.approvalConfigHash),
|
|
1697
|
+
disabled: !!server.disabled,
|
|
1698
|
+
scope: normalizeMcpScope(server.scope),
|
|
1699
|
+
createdAt: numberTimestamp(server.createdAt, Date.now()),
|
|
1700
|
+
updatedAt: numberTimestamp(server.updatedAt, Date.now()),
|
|
1701
|
+
}))
|
|
1702
|
+
: [];
|
|
1703
|
+
return {
|
|
1704
|
+
version: 1,
|
|
1705
|
+
registryPolicy: normalizeRegistryPolicy(value.registryPolicy),
|
|
1706
|
+
scopedPolicies: Array.isArray(value.scopedPolicies)
|
|
1707
|
+
? value.scopedPolicies
|
|
1708
|
+
.filter((policy) => !!policy && typeof policy === 'object' && !!policy.scope)
|
|
1709
|
+
.map((policy) => normalizeScopedPolicy({
|
|
1710
|
+
...policy,
|
|
1711
|
+
scope: normalizePolicyScope(policy.scope),
|
|
1712
|
+
}, policy))
|
|
1713
|
+
: [],
|
|
1714
|
+
servers,
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
writeMcpFile(file) {
|
|
1718
|
+
safeWriteJson(this.mcpServersPath, {
|
|
1719
|
+
version: 1,
|
|
1720
|
+
registryPolicy: normalizeRegistryPolicy(file.registryPolicy),
|
|
1721
|
+
scopedPolicies: file.scopedPolicies
|
|
1722
|
+
.map((policy) => normalizeScopedPolicy({ ...policy, scope: policy.scope }, policy))
|
|
1723
|
+
.sort((left, right) => scopeSpecificity(left.scope) - scopeSpecificity(right.scope) || left.id.localeCompare(right.id)),
|
|
1724
|
+
servers: file.servers.slice().sort((left, right) => left.name.localeCompare(right.name)),
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
async listConfiguredMcpRecords(request) {
|
|
1728
|
+
const hubServers = this.readMcpFile().servers;
|
|
1729
|
+
if (!request.includeSdkConfigured)
|
|
1730
|
+
return hubServers;
|
|
1731
|
+
try {
|
|
1732
|
+
const manager = await this.getMcpManager();
|
|
1733
|
+
const sdkConfigured = manager.loadConfiguredServers(request.cwd)
|
|
1734
|
+
.map((server) => mcpRecordFromConfig(server, server.sourcePath, 'sdk-config'));
|
|
1735
|
+
const hubNames = new Set(hubServers.map((server) => server.name));
|
|
1736
|
+
return [
|
|
1737
|
+
...hubServers,
|
|
1738
|
+
...sdkConfigured.filter((server) => !hubNames.has(server.name)),
|
|
1739
|
+
];
|
|
1740
|
+
}
|
|
1741
|
+
catch {
|
|
1742
|
+
return hubServers;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
mergeMcpServerSummaries(configured, runtimeStates, manager) {
|
|
1746
|
+
const runtimeByName = new Map(runtimeStates.map((state) => [state.config.name, state]));
|
|
1747
|
+
const summaries = [];
|
|
1748
|
+
for (const server of configured) {
|
|
1749
|
+
const state = runtimeByName.get(server.name);
|
|
1750
|
+
summaries.push(this.mcpServerSummary(server, state, manager));
|
|
1751
|
+
runtimeByName.delete(server.name);
|
|
1752
|
+
}
|
|
1753
|
+
for (const state of runtimeByName.values()) {
|
|
1754
|
+
summaries.push(this.mcpServerSummary(mcpRecordFromConfig(state.config, state.config.sourcePath || this.mcpServersPath, 'hub'), state, manager));
|
|
1755
|
+
}
|
|
1756
|
+
return summaries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1757
|
+
}
|
|
1758
|
+
mcpServerSummary(record, state, manager) {
|
|
1759
|
+
const tools = state ? manager.listTools(record.name).map(summarizeMcpTool) : [];
|
|
1760
|
+
const prompts = state ? manager.listPrompts(record.name).map(summarizeMcpPrompt) : [];
|
|
1761
|
+
const resources = state ? manager.listResources(record.name).map(summarizeMcpResource) : [];
|
|
1762
|
+
return {
|
|
1763
|
+
...record,
|
|
1764
|
+
status: state?.status || (record.disabled ? 'skipped' : 'configured'),
|
|
1765
|
+
tools,
|
|
1766
|
+
prompts,
|
|
1767
|
+
resources,
|
|
1768
|
+
trust: analyzeMcpTrust(record),
|
|
1769
|
+
capabilities: state?.capabilities,
|
|
1770
|
+
error: state?.error,
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
loadPluginEvents() {
|
|
1774
|
+
try {
|
|
1775
|
+
if (!existsSync(this.pluginEventLogPath))
|
|
1776
|
+
return [];
|
|
1777
|
+
return readFileSync(this.pluginEventLogPath, 'utf-8')
|
|
1778
|
+
.split(/\r?\n/)
|
|
1779
|
+
.filter(Boolean)
|
|
1780
|
+
.slice(-MAX_PLUGIN_EVENTS)
|
|
1781
|
+
.map((line) => JSON.parse(line));
|
|
1782
|
+
}
|
|
1783
|
+
catch {
|
|
1784
|
+
return [];
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
recordPluginEvent(event) {
|
|
1788
|
+
const record = {
|
|
1789
|
+
id: stableDocumentId('plugin_event', event.pluginName, event.type, event.timestamp, String(this.pluginEvents.length)),
|
|
1790
|
+
type: event.type,
|
|
1791
|
+
pluginName: event.pluginName,
|
|
1792
|
+
timestamp: event.timestamp,
|
|
1793
|
+
data: event.data,
|
|
1794
|
+
};
|
|
1795
|
+
this.pluginEvents.push(record);
|
|
1796
|
+
this.pluginEvents = this.pluginEvents.slice(-MAX_PLUGIN_EVENTS);
|
|
1797
|
+
try {
|
|
1798
|
+
ensureDir(dirname(this.pluginEventLogPath));
|
|
1799
|
+
appendFileSync(this.pluginEventLogPath, `${JSON.stringify(record)}\n`, 'utf-8');
|
|
1800
|
+
}
|
|
1801
|
+
catch {
|
|
1802
|
+
// Event logs are diagnostic; plugin lifecycle should continue if logging fails.
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
appendMcpApproval(record) {
|
|
1806
|
+
try {
|
|
1807
|
+
ensureDir(dirname(this.mcpApprovalsPath));
|
|
1808
|
+
appendFileSync(this.mcpApprovalsPath, `${JSON.stringify(record)}\n`, 'utf-8');
|
|
1809
|
+
}
|
|
1810
|
+
catch {
|
|
1811
|
+
// MCP audit logs are best-effort. Runtime calls surface their own errors.
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
let singleton = null;
|
|
1816
|
+
export function getPluginMcpManagerService() {
|
|
1817
|
+
if (!singleton)
|
|
1818
|
+
singleton = new PluginMcpManagerService();
|
|
1819
|
+
return singleton;
|
|
1820
|
+
}
|
|
1821
|
+
//# sourceMappingURL=PluginMcpManagerService.js.map
|