@datalayer/core 1.1.59 → 1.1.66
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/README.md +2 -2
- package/lib/api/DatalayerApi.d.ts +42 -1
- package/lib/api/DatalayerApi.js +90 -3
- package/lib/api/constants.d.ts +18 -0
- package/lib/api/constants.js +18 -0
- package/lib/api/contents/attachments.d.ts +9 -0
- package/lib/api/contents/attachments.js +43 -0
- package/lib/api/contents/bridges.d.ts +22 -0
- package/lib/api/contents/bridges.js +79 -0
- package/lib/api/contents/capabilities.d.ts +11 -0
- package/lib/api/contents/capabilities.js +21 -0
- package/lib/api/contents/cloudStorage.d.ts +31 -0
- package/lib/api/contents/cloudStorage.js +89 -0
- package/lib/api/contents/dataservers.d.ts +37 -0
- package/lib/api/contents/dataservers.js +121 -0
- package/lib/api/contents/datasets.d.ts +15 -0
- package/lib/api/contents/datasets.js +45 -0
- package/lib/api/contents/datasources.d.ts +70 -0
- package/lib/api/contents/datasources.js +149 -0
- package/lib/api/contents/generated.d.ts +1070 -0
- package/lib/api/contents/index.d.ts +16 -0
- package/lib/api/contents/index.js +20 -0
- package/lib/api/contents/mcp.d.ts +46 -0
- package/lib/api/contents/mcp.js +157 -0
- package/lib/api/contents/objects.d.ts +31 -0
- package/lib/api/contents/objects.js +88 -0
- package/lib/api/contents/operations.d.ts +3 -0
- package/lib/api/contents/operations.js +19 -0
- package/lib/api/contents/runtimeMounts.d.ts +40 -0
- package/lib/api/contents/runtimeMounts.js +59 -0
- package/lib/api/contents/sandboxUid.d.ts +53 -0
- package/lib/api/contents/sandboxUid.js +78 -0
- package/lib/api/contents/sources.d.ts +19 -0
- package/lib/api/contents/sources.js +119 -0
- package/lib/api/contents/sync.d.ts +18 -0
- package/lib/api/contents/sync.js +69 -0
- package/lib/api/contents/transfers.d.ts +38 -0
- package/lib/api/contents/transfers.js +123 -0
- package/lib/api/iam/connectedAgents.d.ts +39 -0
- package/lib/api/iam/connectedAgents.js +52 -0
- package/lib/api/iam/index.d.ts +2 -1
- package/lib/api/iam/index.js +2 -1
- package/lib/api/iam/mcpAlertRules.d.ts +51 -0
- package/lib/api/iam/mcpAlertRules.js +198 -0
- package/lib/api/iam/mcpAuditSettings.d.ts +46 -0
- package/lib/api/iam/mcpAuditSettings.js +120 -0
- package/lib/api/iam/mcpPolicy.d.ts +80 -0
- package/lib/api/iam/mcpPolicy.js +132 -0
- package/lib/api/iam/organizationTeams.d.ts +13 -0
- package/lib/api/iam/organizationTeams.js +43 -0
- package/lib/api/iam/serviceAgents.d.ts +68 -0
- package/lib/api/iam/serviceAgents.js +119 -0
- package/lib/api/index.d.ts +4 -1
- package/lib/api/index.js +3 -1
- package/lib/api/mcp/activity.d.ts +37 -0
- package/lib/api/mcp/activity.js +26 -0
- package/lib/api/mcp/alerts.d.ts +25 -0
- package/lib/api/mcp/alerts.js +51 -0
- package/lib/api/mcp/audit.d.ts +29 -0
- package/lib/api/mcp/audit.js +55 -0
- package/lib/api/mcp/bindings.d.ts +18 -0
- package/lib/api/mcp/bindings.js +42 -0
- package/lib/api/mcp/forwarding.d.ts +25 -0
- package/lib/api/mcp/forwarding.js +46 -0
- package/lib/api/mcp/gateway.d.ts +16 -0
- package/lib/api/mcp/gateway.js +70 -0
- package/lib/api/mcp/generated.d.ts +22 -0
- package/lib/api/mcp/generated.js +213 -0
- package/lib/api/mcp/index.d.ts +17 -0
- package/lib/api/mcp/index.js +25 -0
- package/lib/api/mcp/observability.d.ts +125 -0
- package/lib/api/mcp/observability.js +254 -0
- package/lib/api/mcp/operations.d.ts +83 -0
- package/lib/api/mcp/operations.js +50 -0
- package/lib/api/mcp/organizations.d.ts +102 -0
- package/lib/api/mcp/organizations.js +46 -0
- package/lib/api/mcp/policy.d.ts +9 -0
- package/lib/api/mcp/policy.js +23 -0
- package/lib/api/mcp/tasks.d.ts +49 -0
- package/lib/api/mcp/tasks.js +147 -0
- package/lib/api/utils/validation.d.ts +1 -1
- package/lib/api/utils/validation.js +1 -1
- package/lib/client/auth/index.js +0 -5
- package/lib/client/auth/storage.js +0 -5
- package/lib/client/auth/strategies.js +0 -5
- package/lib/client/base.d.ts +6 -0
- package/lib/client/base.js +8 -0
- package/lib/client/index.d.ts +0 -10
- package/lib/client/index.js +2 -2
- package/lib/client/mixins/ContentsMixin.d.ts +71 -0
- package/lib/client/mixins/ContentsMixin.js +157 -0
- package/lib/client/mixins/IAMMixin.d.ts +0 -31
- package/lib/client/mixins/IAMMixin.js +0 -58
- package/lib/components/animation/AnimatedText.d.ts +1 -0
- package/lib/components/animation/AnimatedText.js +0 -4
- package/lib/components/auth/SigInAPIKey.d.ts +1 -0
- package/lib/components/auth/SigInAPIKey.js +0 -6
- package/lib/components/auth/SignIn.d.ts +1 -0
- package/lib/components/auth/SignIn.js +0 -4
- package/lib/components/auth/SignIn.stories.d.ts +2 -2
- package/lib/components/avatars/BoringAvatar.d.ts +1 -1
- package/lib/components/avatars/BoringAvatar.stories.d.ts +1 -1
- package/lib/components/avatars/DLAvatar.d.ts +1 -1
- package/lib/components/avatars/DLAvatar.js +0 -4
- package/lib/components/avatars/UserAvatar.d.ts +18 -1
- package/lib/components/avatars/UserAvatar.js +14 -14
- package/lib/components/avatars/UserProfileAvatar.d.ts +1 -1
- package/lib/components/banners/NoAutomationBanner.d.ts +1 -1
- package/lib/components/billing/BillingEntitySelect.d.ts +10 -0
- package/lib/components/billing/BillingEntitySelect.js +0 -17
- package/lib/components/billing/index.js +0 -4
- package/lib/components/buttons/DownloadCSVButton.d.ts +1 -1
- package/lib/components/buttons/DownloadJsonButton.d.ts +1 -1
- package/lib/components/buttons/LongActionButton.d.ts +1 -0
- package/lib/components/buttons/LongActionButton.js +0 -4
- package/lib/components/buttons/UploadButton.d.ts +1 -0
- package/lib/components/buttons/UploadButton.js +0 -4
- package/lib/components/checkout/StripeCheckout.d.ts +1 -1
- package/lib/components/collaboration/LiveEditorCollaborators.d.ts +31 -0
- package/lib/components/collaboration/LiveEditorCollaborators.js +98 -53
- package/lib/components/collaboration/index.js +0 -4
- package/lib/components/confetti/ConfettiSuccess.d.ts +1 -1
- package/lib/components/contents/ContentSourceCard.d.ts +8 -0
- package/lib/components/contents/ContentSourceCard.js +23 -0
- package/lib/components/contents/HomeFolderObjectsBrowser.d.ts +10 -0
- package/lib/components/contents/HomeFolderObjectsBrowser.js +148 -0
- package/lib/components/contents/index.d.ts +2 -0
- package/lib/components/contents/index.js +6 -0
- package/lib/components/context/OrganizationSelect.d.ts +1 -1
- package/lib/components/display/AvatarSkeleton.d.ts +1 -1
- package/lib/components/display/BusyDots.d.ts +1 -1
- package/lib/components/display/CenteredSpinner.d.ts +1 -1
- package/lib/components/display/CodePreview.d.ts +1 -1
- package/lib/components/display/DatalayerBox.d.ts +7 -2
- package/lib/components/display/DatalayerBox.js +6 -6
- package/lib/components/display/HorizontalCenter.d.ts +1 -1
- package/lib/components/display/JupyterDialog.d.ts +1 -0
- package/lib/components/display/JupyterDialog.js +1 -7
- package/lib/components/display/Markdown.d.ts +1 -0
- package/lib/components/display/NotebookSkeleton.d.ts +1 -1
- package/lib/components/display/Placeholder.d.ts +1 -1
- package/lib/components/display/ToTopBranded.d.ts +1 -1
- package/lib/components/echarts/EChartsReact.d.ts +1 -1
- package/lib/components/flashes/FlashClosable.d.ts +1 -0
- package/lib/components/flashes/FlashClosable.js +0 -4
- package/lib/components/flashes/FlashGuest.d.ts +1 -1
- package/lib/components/flashes/FlashSurveys.d.ts +1 -1
- package/lib/components/flashes/FlashUnauthorized.d.ts +1 -0
- package/lib/components/flashes/FlashUnauthorized.js +0 -6
- package/lib/components/flashes/surveys/Survey2025_1.d.ts +1 -1
- package/lib/components/iam/ExternalTokenSilentLogin.d.ts +1 -1
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +2 -0
- package/lib/components/labels/StatusLabels.d.ts +10 -0
- package/lib/components/labels/StatusLabels.js +30 -1
- package/lib/components/labels/VisibilityLabel.d.ts +1 -1
- package/lib/components/landings/StepBlock.d.ts +1 -1
- package/lib/components/mcp/ClientBadge.d.ts +41 -0
- package/lib/components/mcp/ClientBadge.js +33 -0
- package/lib/components/mcp/McpErrorBlankslate.d.ts +21 -0
- package/lib/components/mcp/McpErrorBlankslate.js +17 -0
- package/lib/components/mcp/ScopeList.d.ts +28 -0
- package/lib/components/mcp/ScopeList.js +14 -0
- package/lib/components/mcp/index.d.ts +10 -0
- package/lib/components/mcp/index.js +18 -0
- package/lib/components/navbar/NavigationVisbilityObserver.d.ts +2 -1
- package/lib/components/navbar/SubdomainNavBar.d.ts +10 -5
- package/lib/components/navbar/SubdomainNavBar.js +2 -1
- package/lib/components/notebooks/JupyterNotebook.d.ts +1 -1
- package/lib/components/notebooks/JupyterNotebookToolbar.d.ts +1 -1
- package/lib/components/principal/PrincipalAppearance.d.ts +1 -0
- package/lib/components/principal/PrincipalAppearance.js +0 -8
- package/lib/components/principal/PrincipalAvatar.d.ts +9 -1
- package/lib/components/principal/PrincipalAvatar.js +25 -10
- package/lib/components/principal/PrincipalBadge.d.ts +1 -1
- package/lib/components/principal/PrincipalBanner.d.ts +1 -1
- package/lib/components/principal/PrincipalDetailsOverlay.d.ts +31 -1
- package/lib/components/principal/PrincipalDetailsOverlay.js +64 -33
- package/lib/components/principal/PrincipalHoverCard.d.ts +31 -0
- package/lib/components/principal/PrincipalHoverCard.js +52 -0
- package/lib/components/principal/PrincipalSwitcherMenu.d.ts +1 -0
- package/lib/components/principal/PrincipalSwitcherMenu.js +7 -15
- package/lib/components/principal/index.d.ts +2 -1
- package/lib/components/principal/index.js +2 -1
- package/lib/components/progress/ConsumptionBar.d.ts +1 -0
- package/lib/components/progress/ConsumptionBar.js +0 -4
- package/lib/components/progress/ProgressRing.d.ts +1 -0
- package/lib/components/progress/SpinnerBar.d.ts +1 -1
- package/lib/components/progress/Timer.d.ts +1 -0
- package/lib/components/progress/Timer.js +0 -4
- package/lib/components/screencapture/ScreencaptureButton.d.ts +1 -1
- package/lib/components/sharing/ShareAccessComponent.d.ts +15 -1
- package/lib/components/sharing/ShareAccessComponent.js +36 -23
- package/lib/components/sharing/SharingEditor.d.ts +17 -0
- package/lib/components/sharing/SharingEditor.js +0 -24
- package/lib/components/sparklines/Sparklines.js +3 -4
- package/lib/components/sparklines/index.js +0 -4
- package/lib/components/storage/ContentsBrowser.d.ts +25 -1
- package/lib/components/storage/ContentsBrowser.js +145 -52
- package/lib/components/storage/ContentsBrowserMock.d.ts +3 -0
- package/lib/components/storage/ContentsBrowserMock.js +75 -0
- package/lib/components/storage/ContentsItems.d.ts +2 -1
- package/lib/components/storage/ContentsItems.js +0 -4
- package/lib/components/storage/ContentsTable.d.ts +50 -0
- package/lib/components/storage/ContentsTable.js +134 -0
- package/lib/components/storage/HomeFolderDrive.d.ts +71 -0
- package/lib/components/storage/HomeFolderDrive.js +248 -0
- package/lib/components/storage/index.d.ts +2 -0
- package/lib/components/storage/index.js +2 -0
- package/lib/components/students/StudentItemStatus.d.ts +1 -1
- package/lib/components/students/StudentResultCircle.d.ts +1 -1
- package/lib/components/subnav/SubNav.d.ts +5 -5
- package/lib/components/subnav/SubNav.js +11 -9
- package/lib/components/tables/DataTable.d.ts +1 -1
- package/lib/components/text-reveal/TextRevealAnimation.d.ts +2 -2
- package/lib/components/text-reveal/useTextRevealAnimationLines.d.ts +1 -1
- package/lib/components/time/LiveRelativeTime.d.ts +1 -0
- package/lib/components/time/LiveRelativeTime.js +0 -8
- package/lib/components/time/index.js +0 -4
- package/lib/components/timeline/Timeline.d.ts +1 -1
- package/lib/components/timeline/index.js +0 -4
- package/lib/components/users/PeerIndicator.d.ts +1 -0
- package/lib/components/users/PeerIndicator.js +0 -4
- package/lib/config/Configuration.d.ts +15 -17
- package/lib/config/Configuration.js +7 -0
- package/lib/hooks/index.d.ts +2 -0
- package/lib/hooks/index.js +2 -0
- package/lib/hooks/layouts/LayoutBackdrop.d.ts +1 -1
- package/lib/hooks/layouts/LayoutScreencapture.d.ts +1 -1
- package/lib/hooks/useBackdrop.d.ts +1 -1
- package/lib/hooks/useBackdropJupyterLab.d.ts +3 -2
- package/lib/hooks/useBackdropJupyterLab.js +0 -4
- package/lib/hooks/useCache.d.ts +310 -105
- package/lib/hooks/useCache.js +142 -355
- package/lib/hooks/useContainsFocus.d.ts +1 -1
- package/lib/hooks/useContents.d.ts +313 -0
- package/lib/hooks/useContents.js +964 -0
- package/lib/hooks/useFocusTrap.d.ts +4 -4
- package/lib/hooks/useFocusTrap.js +1 -1
- package/lib/hooks/useMcp.d.ts +229 -0
- package/lib/hooks/useMcp.js +656 -0
- package/lib/hooks/useMobile.js +0 -5
- package/lib/hooks/useProjectStore.d.ts +3 -3
- package/lib/hooks/useProjectStore.js +4 -4
- package/lib/hooks/useRef.d.ts +1 -1
- package/lib/hooks/useRef.js +3 -0
- package/lib/hooks/useScreencapture.d.ts +2 -1
- package/lib/hooks/useScreencapture.js +0 -4
- package/lib/hooks/useToast.js +0 -4
- package/lib/hooks/useUser.js +42 -0
- package/lib/hooks/useVisibilityObserver.d.ts +1 -1
- package/lib/index.d.ts +4 -1
- package/lib/index.js +3 -1
- package/lib/mocks/components/FlashMock.d.ts +1 -1
- package/lib/mocks/views/ActionMenuMock.d.ts +1 -1
- package/lib/mocks/views/ChartMock.d.ts +1 -1
- package/lib/mocks/views/DashboardMock.d.ts +1 -1
- package/lib/mocks/views/FormMock.d.ts +1 -1
- package/lib/mocks/views/TableMock.d.ts +1 -1
- package/lib/mocks/views/WipMock.d.ts +1 -1
- package/lib/models/Credits.d.ts +16 -0
- package/lib/models/Credits.js +22 -0
- package/lib/models/McpAuditEvent.d.ts +52 -0
- package/lib/models/McpAuditEvent.js +5 -0
- package/lib/models/McpBinding.d.ts +51 -0
- package/lib/models/McpBinding.js +5 -0
- package/lib/models/McpPolicy.d.ts +77 -0
- package/lib/models/McpPolicy.js +5 -0
- package/lib/models/McpTask.d.ts +94 -0
- package/lib/models/McpTask.js +7 -0
- package/lib/models/StartedBy.d.ts +57 -0
- package/lib/models/StartedBy.js +81 -0
- package/lib/models/UserDTO.js +4 -2
- package/lib/models/contents/__fixtures__/solr-codec.json +57 -0
- package/lib/models/contents/__fixtures__/v1-contracts.json +66 -0
- package/lib/models/contents/fieldCase.d.ts +8 -0
- package/lib/models/contents/fieldCase.js +37 -0
- package/lib/models/contents/index.d.ts +2 -0
- package/lib/models/contents/index.js +6 -0
- package/lib/models/contents/permissions.d.ts +16 -0
- package/lib/models/contents/permissions.js +34 -0
- package/lib/models/index.d.ts +5 -1
- package/lib/models/index.js +5 -1
- package/lib/state/substates/CoreState.d.ts +15 -7
- package/lib/state/substates/CoreState.js +18 -14
- package/lib/state/substates/IAMState.d.ts +1 -1
- package/lib/state/substates/IAMState.js +5 -5
- package/lib/state/substates/ProfileState.js +0 -4
- package/lib/state/substates/index.d.ts +0 -1
- package/lib/state/substates/index.js +0 -1
- package/lib/utils/Jwt.d.ts +26 -0
- package/lib/utils/Jwt.js +37 -0
- package/lib/utils/WithSuspense.d.ts +1 -1
- package/lib/views/api-keys/APIKeyEdit.d.ts +1 -1
- package/lib/views/api-keys/APIKeyNew.d.ts +1 -1
- package/lib/views/api-keys/APIKeys.d.ts +1 -1
- package/lib/views/api-keys/APIKeys.js +30 -4
- package/lib/views/api-keys/APIKeysStandalone.d.ts +5 -1
- package/lib/views/api-keys/APIKeysStandalone.js +2 -2
- package/lib/views/datasources/DatasourceDetail.d.ts +12 -5
- package/lib/views/datasources/DatasourceDetail.js +80 -110
- package/lib/views/datasources/DatasourceNew.d.ts +14 -8
- package/lib/views/datasources/DatasourceNew.js +129 -118
- package/lib/views/datasources/Datasources.d.ts +39 -7
- package/lib/views/datasources/Datasources.js +95 -40
- package/lib/views/datasources/DatasourcesMock.d.ts +3 -0
- package/lib/views/datasources/DatasourcesMock.js +57 -0
- package/lib/views/iam/SignInSimple.d.ts +12 -0
- package/lib/views/iam/SignInSimple.js +4 -4
- package/lib/views/index.d.ts +1 -0
- package/lib/views/index.js +1 -0
- package/lib/views/mcp/AlertDestinations.d.ts +34 -0
- package/lib/views/mcp/AlertDestinations.js +75 -0
- package/lib/views/mcp/AlertRules.d.ts +32 -0
- package/lib/views/mcp/AlertRules.js +245 -0
- package/lib/views/mcp/AuditLog.d.ts +42 -0
- package/lib/views/mcp/AuditLog.js +144 -0
- package/lib/views/mcp/ConnectedAgents.d.ts +34 -0
- package/lib/views/mcp/ConnectedAgents.js +100 -0
- package/lib/views/mcp/EnterpriseConsole.d.ts +74 -0
- package/lib/views/mcp/EnterpriseConsole.js +173 -0
- package/lib/views/mcp/McpDashboard.d.ts +50 -0
- package/lib/views/mcp/McpDashboard.js +347 -0
- package/lib/views/mcp/McpHome.d.ts +92 -0
- package/lib/views/mcp/McpHome.js +153 -0
- package/lib/views/mcp/McpObservability.d.ts +50 -0
- package/lib/views/mcp/McpObservability.js +150 -0
- package/lib/views/mcp/OrganizationPolicy.d.ts +45 -0
- package/lib/views/mcp/OrganizationPolicy.js +107 -0
- package/lib/views/mcp/OrganizationUsage.d.ts +69 -0
- package/lib/views/mcp/OrganizationUsage.js +113 -0
- package/lib/views/mcp/PersonalPolicy.d.ts +54 -0
- package/lib/views/mcp/PersonalPolicy.js +125 -0
- package/lib/views/mcp/Policies.d.ts +53 -0
- package/lib/views/mcp/Policies.js +210 -0
- package/lib/views/mcp/PolicyForm.d.ts +67 -0
- package/lib/views/mcp/PolicyForm.js +91 -0
- package/lib/views/mcp/PolicyHistory.d.ts +45 -0
- package/lib/views/mcp/PolicyHistory.js +62 -0
- package/lib/views/mcp/ServiceAgents.d.ts +46 -0
- package/lib/views/mcp/ServiceAgents.js +194 -0
- package/lib/views/mcp/TeamPolicies.d.ts +33 -0
- package/lib/views/mcp/TeamPolicies.js +98 -0
- package/lib/views/mcp/format.d.ts +25 -0
- package/lib/views/mcp/format.js +77 -0
- package/lib/views/mcp/index.d.ts +29 -0
- package/lib/views/mcp/index.js +37 -0
- package/lib/views/mcp/types.d.ts +66 -0
- package/lib/views/mcp/types.js +5 -0
- package/lib/views/secrets/SecretEdit.d.ts +5 -1
- package/lib/views/secrets/SecretEdit.js +2 -2
- package/lib/views/secrets/SecretNew.d.ts +11 -1
- package/lib/views/secrets/SecretNew.js +2 -2
- package/lib/views/secrets/Secrets.d.ts +12 -1
- package/lib/views/secrets/Secrets.js +2 -2
- package/package.json +28 -24
- package/scripts/generate-contents-types.py +169 -0
- package/scripts/generate-mcp-types.py +233 -0
- package/lib/api/iam/datasources.d.ts +0 -86
- package/lib/api/iam/datasources.js +0 -185
- package/lib/components/toolbars/index.d.ts +0 -1
- package/lib/models/Datasource.d.ts +0 -181
- package/lib/models/Datasource.js +0 -154
- package/lib/state/substates/DatasourceState.d.ts +0 -11
- package/lib/state/substates/DatasourceState.js +0 -16
- /package/lib/{components/toolbars/index.js → api/contents/generated.js} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Button, Flash, Heading, Spinner, Text } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { McpErrorBlankslate } from '../../components/mcp/index.js';
|
|
6
|
+
import { useDeleteMcpPolicyLayer, useMcpPolicyLayer, useSetMcpPolicyLayer, } from '../../hooks/useMcp.js';
|
|
7
|
+
import { useToast } from '../../hooks/index.js';
|
|
8
|
+
import { McpPolicyConflict, } from '../../api/iam/mcpPolicy.js';
|
|
9
|
+
import { EMPTY_POLICY_DRAFT, PolicyForm, draftOf, policyChanged, rulesFrom, } from './PolicyForm.js';
|
|
10
|
+
/** A list rule already denied elsewhere, as a sentence naming who. */
|
|
11
|
+
export const deniedElsewhere = (typed, organizations) => {
|
|
12
|
+
const wanted = typed
|
|
13
|
+
.split('\n')
|
|
14
|
+
.map(entry => entry.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
const said = [];
|
|
17
|
+
for (const organization of organizations) {
|
|
18
|
+
const denied = (organization.rules?.toolDenylist ?? []).filter(tool => wanted.includes(tool));
|
|
19
|
+
if (denied.length > 0) {
|
|
20
|
+
said.push(`${organization.name} denies ${denied.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return said;
|
|
24
|
+
};
|
|
25
|
+
/** A number rule already capped lower elsewhere. */
|
|
26
|
+
export const cappedLower = (typed, organizations, rule) => {
|
|
27
|
+
const value = Number(typed.trim());
|
|
28
|
+
// The blank check is belt-and-braces rather than load-bearing: `Number('')`
|
|
29
|
+
// is 0, and no positive organization cap is below 0, so a blank field says
|
|
30
|
+
// nothing either way today. It is kept because that depends on the
|
|
31
|
+
// `theirs > 0` guard below, and a reader changing one should not have to
|
|
32
|
+
// notice it was holding up the other.
|
|
33
|
+
if (!typed.trim() || !Number.isFinite(value)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const said = [];
|
|
37
|
+
for (const organization of organizations) {
|
|
38
|
+
const theirs = organization.rules?.[rule];
|
|
39
|
+
if (typeof theirs === 'number' && theirs > 0 && theirs < value) {
|
|
40
|
+
said.push(`${organization.name} caps it at ${theirs}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return said;
|
|
44
|
+
};
|
|
45
|
+
export const PersonalPolicy = ({ errorState, userUid, organizations = [], showTitle = true, }) => {
|
|
46
|
+
const { enqueueToast } = useToast();
|
|
47
|
+
const layer = useMcpPolicyLayer('personal', userUid);
|
|
48
|
+
const save = useSetMcpPolicyLayer('personal', userUid);
|
|
49
|
+
const remove = useDeleteMcpPolicyLayer('personal', userUid);
|
|
50
|
+
const [draft, setDraft] = useState(EMPTY_POLICY_DRAFT);
|
|
51
|
+
const [conflict, setConflict] = useState('');
|
|
52
|
+
const [refusal, setRefusal] = useState('');
|
|
53
|
+
const stored = useMemo(() => draftOf(layer.data), [layer.data]);
|
|
54
|
+
useEffect(() => setDraft(stored), [stored]);
|
|
55
|
+
const changed = useMemo(() => policyChanged(draft, stored), [draft, stored]);
|
|
56
|
+
const set = (key, value) => setDraft(current => ({ ...current, [key]: value }));
|
|
57
|
+
/**
|
|
58
|
+
* What to say beside each field, and nothing where there is nothing to
|
|
59
|
+
* say. Recomputed as they type, because the point is to be read while
|
|
60
|
+
* the value is still being chosen.
|
|
61
|
+
*/
|
|
62
|
+
const notes = useMemo(() => {
|
|
63
|
+
if (organizations.length === 0) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
const built = {};
|
|
67
|
+
const permitted = deniedElsewhere(draft.toolAllowlist, organizations);
|
|
68
|
+
if (permitted.length > 0) {
|
|
69
|
+
built.toolAllowlist = `Permitting a tool here does not lift a denial: ${permitted.join('; ')}. Your agents still cannot use those when acting for them.`;
|
|
70
|
+
}
|
|
71
|
+
for (const key of [
|
|
72
|
+
'maxCallsPerMinute',
|
|
73
|
+
'maxCreditsPerDay',
|
|
74
|
+
'maxConcurrentSandboxes',
|
|
75
|
+
]) {
|
|
76
|
+
const lower = cappedLower(draft[key], organizations, key);
|
|
77
|
+
if (lower.length > 0) {
|
|
78
|
+
built[key] =
|
|
79
|
+
`Wider than an organization you belong to allows — ${lower.join('; ')}. When you act for them, theirs applies; this still governs your own work.`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return built;
|
|
83
|
+
}, [draft, organizations]);
|
|
84
|
+
const apply = () => {
|
|
85
|
+
const rules = rulesFrom(draft);
|
|
86
|
+
if (typeof rules === 'string') {
|
|
87
|
+
setRefusal(rules);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
setRefusal('');
|
|
91
|
+
setConflict('');
|
|
92
|
+
save.mutate({ rules, expectedVersion: layer.data?.version }, {
|
|
93
|
+
onSuccess: () => enqueueToast('Saved. It applies to your next call.', {
|
|
94
|
+
variant: 'success',
|
|
95
|
+
}),
|
|
96
|
+
onError: error => {
|
|
97
|
+
if (error instanceof McpPolicyConflict) {
|
|
98
|
+
setConflict(error.message);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
enqueueToast(`Could not save: ${error.message}`, {
|
|
102
|
+
variant: 'error',
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
if (layer.isError) {
|
|
108
|
+
return (_jsx(McpErrorBlankslate, { state: errorState(layer.error, 'Your MCP policy'), onRetry: () => layer.refetch() }));
|
|
109
|
+
}
|
|
110
|
+
if (layer.isPending && layer.data === undefined) {
|
|
111
|
+
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 5 }, children: _jsx(Spinner, {}) }));
|
|
112
|
+
}
|
|
113
|
+
return (_jsxs(Box, { sx: { display: 'grid', gap: 3, minWidth: 0, maxWidth: '52rem' }, children: [showTitle && (_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Your MCP policy" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, children: ["Rules for your own agents. These can only make things", ' ', _jsx("strong", { children: "stricter" }), " \u2014 where you belong to an organization, its rules apply underneath and nothing here can loosen them."] })] })), conflict && (_jsxs(Flash, { variant: "warning", children: [_jsx(Text, { sx: { fontSize: 1 }, children: conflict }), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Button, { size: "small", onClick: () => {
|
|
114
|
+
setConflict('');
|
|
115
|
+
layer.refetch();
|
|
116
|
+
}, children: "Read it again" }) })] })), refusal && (_jsx(Flash, { variant: "danger", children: _jsx(Text, { sx: { fontSize: 1 }, children: refusal }) })), _jsx(PolicyForm, { draft: draft, onChange: set, notes: notes }), _jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center' }, children: [_jsx(Button, { variant: "primary", onClick: apply, disabled: !changed || save.isPending, children: "Save" }), changed && (_jsx(Button, { variant: "invisible", onClick: () => setDraft(stored), children: "Discard changes" })), layer.data && (_jsx(Box, { sx: { marginLeft: 'auto' }, children: _jsx(Button, { variant: "danger", disabled: remove.isPending, onClick: () => remove.mutate(undefined, {
|
|
117
|
+
onSuccess: () => enqueueToast('Removed. You narrow nothing of your own.', {
|
|
118
|
+
variant: 'success',
|
|
119
|
+
}),
|
|
120
|
+
onError: error => enqueueToast(`Could not remove: ${error.message}`, {
|
|
121
|
+
variant: 'error',
|
|
122
|
+
}),
|
|
123
|
+
}), children: "Remove my policy" }) }))] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.subtle' }, children: "Removing yours does not widen anything your organization set. It only stops your own rules from narrowing further." })] }));
|
|
124
|
+
};
|
|
125
|
+
export default PersonalPolicy;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Jupyter MCP Server lets a person's agents do — and who decided.
|
|
3
|
+
*
|
|
4
|
+
* Policy is layered and each layer only narrows: the platform's defaults,
|
|
5
|
+
* then the organization's rules for an organization-scope token, then the
|
|
6
|
+
* person's own. The gateway answers the **effective** policy for a token
|
|
7
|
+
* with, per rule, the layer that decided it, which is what turns "why was
|
|
8
|
+
* my agent refused" from a support question into a line on this page.
|
|
9
|
+
*
|
|
10
|
+
* Read-only, always: this table is the answer, not the form. The personal
|
|
11
|
+
* rules a person may narrow for themselves, and the forms for the
|
|
12
|
+
* organizations they own, are the two sections beneath it and arrive with
|
|
13
|
+
* the policy layers in milestone 3 — until then the gateway answers the
|
|
14
|
+
* platform defaults and the token's scopes, and every row says so.
|
|
15
|
+
*
|
|
16
|
+
* @module views/mcp/Policies
|
|
17
|
+
*/
|
|
18
|
+
import type { JSX } from 'react';
|
|
19
|
+
import type { McpPolicyLayer } from '../../models/McpPolicy';
|
|
20
|
+
import { type McpErrorStateFn, type McpRoutes } from './types';
|
|
21
|
+
export interface PoliciesProps {
|
|
22
|
+
/** The application's words for a failed request. */
|
|
23
|
+
errorState: McpErrorStateFn;
|
|
24
|
+
/** Where this application puts the surfaces this view links to. */
|
|
25
|
+
routes: McpRoutes;
|
|
26
|
+
/** The organization whose name a row can carry, when the token has one. */
|
|
27
|
+
organizationName?: string;
|
|
28
|
+
showTitle?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The agent a preview is taken as, or nothing for the reader's own token.
|
|
32
|
+
*
|
|
33
|
+
* Empty is the reader's own, which is the default and the honest one: the
|
|
34
|
+
* table answers "what applies to me" until somebody asks otherwise.
|
|
35
|
+
*/
|
|
36
|
+
export type PolicyPreviewAgent = string;
|
|
37
|
+
/**
|
|
38
|
+
* The filter for a preview choice.
|
|
39
|
+
*
|
|
40
|
+
* A blank choice is *nobody*, not an agent whose client id is the empty
|
|
41
|
+
* string: sent as a filter that matches nothing, and a table that renders
|
|
42
|
+
* empty looks like a policy that grants nothing rather than a query that
|
|
43
|
+
* asked for no one.
|
|
44
|
+
*/
|
|
45
|
+
export declare const policyFiltersOf: (previewAs: PolicyPreviewAgent) => {
|
|
46
|
+
agent: string | undefined;
|
|
47
|
+
};
|
|
48
|
+
/** How each layer is named on a row, in the reader's terms. */
|
|
49
|
+
export declare const layerLabel: (layer: McpPolicyLayer, organizationName?: string) => string;
|
|
50
|
+
/** A rule's value in the words of the page, whatever shape it arrived in. */
|
|
51
|
+
export declare const ruleValueLabel: (value: unknown) => string;
|
|
52
|
+
export declare const Policies: ({ errorState, routes, organizationName, showTitle, }: PoliciesProps) => JSX.Element;
|
|
53
|
+
export default Policies;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { Heading, Label, Link, Select, Spinner, Text } from '@primer/react';
|
|
4
|
+
import { Blankslate, Table } from '@primer/react/experimental';
|
|
5
|
+
import { Box } from '@datalayer/primer-addons';
|
|
6
|
+
import { ShieldCheckIcon } from '@primer/octicons-react';
|
|
7
|
+
import { McpErrorBlankslate } from '../../components/mcp/index.js';
|
|
8
|
+
import { useConnectedAgents, useEffectivePolicy } from '../../hooks/useMcp.js';
|
|
9
|
+
import { useNavigate } from '../../hooks/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* The filter for a preview choice.
|
|
12
|
+
*
|
|
13
|
+
* A blank choice is *nobody*, not an agent whose client id is the empty
|
|
14
|
+
* string: sent as a filter that matches nothing, and a table that renders
|
|
15
|
+
* empty looks like a policy that grants nothing rather than a query that
|
|
16
|
+
* asked for no one.
|
|
17
|
+
*/
|
|
18
|
+
export const policyFiltersOf = (previewAs) => ({
|
|
19
|
+
agent: previewAs.trim() || undefined,
|
|
20
|
+
});
|
|
21
|
+
/** How each layer is named on a row, in the reader's terms. */
|
|
22
|
+
export const layerLabel = (layer, organizationName) => {
|
|
23
|
+
switch (layer) {
|
|
24
|
+
case 'platform':
|
|
25
|
+
return 'Datalayer default';
|
|
26
|
+
case 'organization':
|
|
27
|
+
return organizationName
|
|
28
|
+
? `required by ${organizationName} (organization)`
|
|
29
|
+
: 'required by your organization';
|
|
30
|
+
case 'team':
|
|
31
|
+
return 'required by your team';
|
|
32
|
+
default:
|
|
33
|
+
return 'you asked';
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const LAYER_VARIANT = {
|
|
37
|
+
platform: 'secondary',
|
|
38
|
+
organization: 'accent',
|
|
39
|
+
team: 'accent',
|
|
40
|
+
personal: 'attention',
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The rules this page names, in the order it names them, with the words a
|
|
44
|
+
* reader uses rather than the field names of the policy document.
|
|
45
|
+
*/
|
|
46
|
+
const ROW_ORDER = [
|
|
47
|
+
{
|
|
48
|
+
name: 'approval_policy',
|
|
49
|
+
label: 'Default approval',
|
|
50
|
+
description: 'What happens before a tool your agent calls actually runs.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'allowed_clients',
|
|
54
|
+
label: 'Clients admitted',
|
|
55
|
+
description: 'Which MCP clients may hold a grant at all.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'require_org_sso',
|
|
59
|
+
label: 'Sign-in required',
|
|
60
|
+
description: 'Whether an agent must be authorized through your organization.',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'require_dpop',
|
|
64
|
+
label: 'Token binding',
|
|
65
|
+
description: 'Whether a token must be bound to the key of the client holding it.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'max_scopes',
|
|
69
|
+
label: 'Scopes at most',
|
|
70
|
+
description: 'The ceiling on what any agent of yours may be granted.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'allowed_environments',
|
|
74
|
+
label: 'Compute: environments',
|
|
75
|
+
description: 'The environments an agent may launch a sandbox in.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'allowed_providers',
|
|
79
|
+
label: 'Compute: providers',
|
|
80
|
+
description: 'Where those sandboxes may run.',
|
|
81
|
+
},
|
|
82
|
+
{ name: 'max_gpu', label: 'Compute: GPU at most', description: '' },
|
|
83
|
+
{
|
|
84
|
+
name: 'max_reservation_minutes',
|
|
85
|
+
label: 'Compute: reservation at most',
|
|
86
|
+
description: '',
|
|
87
|
+
},
|
|
88
|
+
{ name: 'credits_per_day', label: 'Spend: credits a day', description: '' },
|
|
89
|
+
{ name: 'calls_per_minute', label: 'Rate: calls a minute', description: '' },
|
|
90
|
+
{ name: 'session_max_hours', label: 'Session at most', description: '' },
|
|
91
|
+
];
|
|
92
|
+
/** A rule's value in the words of the page, whatever shape it arrived in. */
|
|
93
|
+
export const ruleValueLabel = (value) => {
|
|
94
|
+
if (value === null || value === undefined) {
|
|
95
|
+
return 'No limit';
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === 'boolean') {
|
|
98
|
+
return value ? 'Required' : 'Not required';
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value.length === 0 ? 'Nothing admitted' : value.join(', ');
|
|
102
|
+
}
|
|
103
|
+
if (typeof value === 'object') {
|
|
104
|
+
return Object.entries(value)
|
|
105
|
+
.map(([key, item]) => `${key}: ${String(item)}`)
|
|
106
|
+
.join(' · ');
|
|
107
|
+
}
|
|
108
|
+
return String(value);
|
|
109
|
+
};
|
|
110
|
+
/** The tools of one access kind, and what happens before they run. */
|
|
111
|
+
const ToolGroup = ({ title, description, tools, organizationName, }) => (_jsxs(Box, { sx: { display: 'grid', gap: 2 }, children: [_jsxs(Box, { children: [_jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold', display: 'block' }, children: title }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: description })] }), tools.length === 0 ? (_jsx(Text, { sx: { fontSize: 0, color: 'fg.subtle' }, children: "No tool of this kind is available to your agents." })) : (_jsx(Box, { sx: { display: 'flex', gap: 1, flexWrap: 'wrap' }, children: tools.map(tool => (_jsx(Label, { size: "small", variant: !tool.allowed
|
|
112
|
+
? 'danger'
|
|
113
|
+
: tool.approval === 'explicit'
|
|
114
|
+
? 'attention'
|
|
115
|
+
: 'secondary', title: `${tool.scope} · ${tool.approval ?? 'no approval rule'} · ${layerLabel(tool.decidedBy, organizationName)}`, children: tool.tool }, tool.tool))) }))] }));
|
|
116
|
+
export const Policies = ({ errorState, routes, organizationName, showTitle = true, }) => {
|
|
117
|
+
const navigate = useNavigate();
|
|
118
|
+
// Whose policy is being read. Empty is the reader's own.
|
|
119
|
+
//
|
|
120
|
+
// The layers differ per agent — a client admitted by one organization and
|
|
121
|
+
// not another, a scope one grant carries and the next does not — so "what
|
|
122
|
+
// applies" has no single answer once somebody has connected more than one
|
|
123
|
+
// agent. Without this the page answered for the reader's own token and
|
|
124
|
+
// said nothing about which, which is the answer to a question nobody
|
|
125
|
+
// asked.
|
|
126
|
+
const [previewAs, setPreviewAs] = useState('');
|
|
127
|
+
const agents = useConnectedAgents();
|
|
128
|
+
const policy = useEffectivePolicy(policyFiltersOf(previewAs).agent);
|
|
129
|
+
const rows = useMemo(() => {
|
|
130
|
+
const effective = policy.data;
|
|
131
|
+
if (!effective) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const byName = new Map(effective.rules.map(rule => [rule.name, rule]));
|
|
135
|
+
const known = ROW_ORDER.filter(row => byName.has(row.name)).map(row => ({
|
|
136
|
+
...row,
|
|
137
|
+
rule: byName.get(row.name),
|
|
138
|
+
}));
|
|
139
|
+
// A rule the gateway answers that this page has no wording for is still
|
|
140
|
+
// shown: hiding it would make the page a partial answer to "what applies".
|
|
141
|
+
const rest = effective.rules
|
|
142
|
+
.filter(rule => !ROW_ORDER.some(row => row.name === rule.name))
|
|
143
|
+
.map(rule => ({
|
|
144
|
+
name: rule.name,
|
|
145
|
+
label: rule.name,
|
|
146
|
+
description: '',
|
|
147
|
+
rule,
|
|
148
|
+
}));
|
|
149
|
+
return [...known, ...rest];
|
|
150
|
+
}, [policy.data]);
|
|
151
|
+
const tools = policy.data?.tools ?? [];
|
|
152
|
+
const readOnlyTools = tools.filter(tool => tool.access === 'read');
|
|
153
|
+
const writeTools = tools.filter(tool => tool.access === 'write');
|
|
154
|
+
const executeTools = tools.filter(tool => tool.access === 'execute' || !tool.access);
|
|
155
|
+
if (policy.isError) {
|
|
156
|
+
return (_jsx(McpErrorBlankslate, { state: errorState(policy.error, 'Policies'), onRetry: () => policy.refetch() }));
|
|
157
|
+
}
|
|
158
|
+
return (_jsxs(Box, { sx: { display: 'grid', gap: 4, minWidth: 0 }, children: [showTitle && (_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Policies" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, children: previewAs
|
|
159
|
+
? 'What applies to this agent, and which layer decided it.'
|
|
160
|
+
: 'What applies to your agents, and which layer decided it.' })] })), (agents.data?.length ?? 0) > 1 && (_jsxs(Box, { sx: {
|
|
161
|
+
display: 'flex',
|
|
162
|
+
alignItems: 'center',
|
|
163
|
+
gap: 2,
|
|
164
|
+
flexWrap: 'wrap',
|
|
165
|
+
}, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Preview as" }), _jsx(Box, { sx: { minWidth: '18rem' }, children: _jsxs(Select, { value: previewAs, onChange: event => setPreviewAs(event.target.value), "aria-label": "Preview the policy as", children: [_jsx(Select.Option, { value: "", children: "You \u2014 your own token" }), (agents.data ?? []).map(agent => (_jsx(Select.Option, { value: agent.clientId, children: agent.clientName || agent.clientId }, agent.uid)))] }) }), previewAs && (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "A grant carries its own scopes and its own admitted clients, so this can differ from what applies to you." }))] })), policy.isPending && !policy.data ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 6 }, children: _jsx(Spinner, {}) })) : rows.length === 0 && tools.length === 0 ? (_jsxs(Blankslate, { border: true, spacious: true, children: [_jsx(Blankslate.Visual, { children: _jsx(ShieldCheckIcon, { size: "medium" }) }), _jsx(Blankslate.Heading, { children: "The defaults apply" }), _jsx(Blankslate.Description, { children: _jsx(Text, { sx: { textAlign: 'center' }, children: "No agent is connected yet, so nothing narrows the Datalayer defaults. Connect a client and this page fills with what its calls are allowed to do." }) }), _jsx(Link, { as: "button", sx: { cursor: 'pointer' }, onClick: () => navigate(routes.access), children: "Set up a client" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: { display: 'grid', gap: 2 }, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2 }, children: "What applies to your agents" }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["Scope of this reading: ", policy.data?.scope ?? 'personal', policy.data?.evaluatedAt
|
|
166
|
+
? ` · evaluated ${policy.data.evaluatedAt}`
|
|
167
|
+
: ''] })] }), _jsxs(Table.Container, { children: [_jsx(Table.Title, { as: "h4", id: "effective-policy", children: "Effective policy" }), _jsx(Table.Subtitle, { as: "p", id: "effective-policy-subtitle", children: "Each layer only narrows the one above it, so the strictest rule is what applies." }), _jsxs(Box, { as: "table", "aria-labelledby": "effective-policy", "aria-describedby": "effective-policy-subtitle", sx: { width: '100%', borderCollapse: 'collapse' }, children: [_jsx(Box, { as: "thead", children: _jsx(Box, { as: "tr", children: ['Rule', 'What applies', 'Decided by'].map(header => (_jsx(Box, { as: "th", sx: {
|
|
168
|
+
textAlign: 'left',
|
|
169
|
+
fontSize: 0,
|
|
170
|
+
color: 'fg.muted',
|
|
171
|
+
fontWeight: 'semibold',
|
|
172
|
+
py: 2,
|
|
173
|
+
px: 2,
|
|
174
|
+
borderBottom: '1px solid',
|
|
175
|
+
borderColor: 'border.default',
|
|
176
|
+
}, children: header }, header))) }) }), _jsx(Box, { as: "tbody", children: rows.map(row => (_jsxs(Box, { as: "tr", children: [_jsxs(Box, { as: "td", sx: {
|
|
177
|
+
py: 2,
|
|
178
|
+
px: 2,
|
|
179
|
+
borderBottom: '1px solid',
|
|
180
|
+
borderColor: 'border.muted',
|
|
181
|
+
verticalAlign: 'top',
|
|
182
|
+
}, children: [_jsx(Text, { sx: { fontSize: 1, display: 'block' }, children: row.label }), row.description && (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: row.description }))] }), _jsx(Box, { as: "td", sx: {
|
|
183
|
+
py: 2,
|
|
184
|
+
px: 2,
|
|
185
|
+
borderBottom: '1px solid',
|
|
186
|
+
borderColor: 'border.muted',
|
|
187
|
+
verticalAlign: 'top',
|
|
188
|
+
}, children: _jsx(Text, { sx: { fontSize: 1 }, children: ruleValueLabel(row.rule.value) }) }), _jsxs(Box, { as: "td", sx: {
|
|
189
|
+
py: 2,
|
|
190
|
+
px: 2,
|
|
191
|
+
borderBottom: '1px solid',
|
|
192
|
+
borderColor: 'border.muted',
|
|
193
|
+
verticalAlign: 'top',
|
|
194
|
+
whiteSpace: 'nowrap',
|
|
195
|
+
}, children: [_jsx(Label, { size: "small", variant: LAYER_VARIANT[row.rule.decidedBy], children: layerLabel(row.rule.decidedBy, organizationName) }), row.rule.reason && (_jsx(Text, { sx: {
|
|
196
|
+
fontSize: 0,
|
|
197
|
+
color: 'fg.muted',
|
|
198
|
+
display: 'block',
|
|
199
|
+
mt: 1,
|
|
200
|
+
}, children: row.rule.reason }))] })] }, row.name))) })] })] }), _jsxs(Box, { sx: { display: 'grid', gap: 3 }, children: [_jsx(ToolGroup, { title: "Read-only tools", description: "Tools that only look: listing, reading a cell, reading a source.", tools: readOnlyTools, organizationName: organizationName }), _jsx(ToolGroup, { title: "Tools that change things", description: "Tools that write to a notebook, a document or a source.", tools: writeTools, organizationName: organizationName }), _jsx(ToolGroup, { title: "Tools that launch or spend", description: "Tools that execute code, launch a sandbox or reserve compute.", tools: executeTools, organizationName: organizationName })] }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["Your token carries ", policy.data?.scopes.length ?? 0, " scopes, which is the ceiling over every rule above:", ' ', (policy.data?.scopes ?? []).join(', ') || 'none', "."] })] })), _jsxs(Box, { sx: {
|
|
201
|
+
p: 3,
|
|
202
|
+
border: '1px solid',
|
|
203
|
+
borderColor: 'border.default',
|
|
204
|
+
borderRadius: 2,
|
|
205
|
+
bg: 'canvas.subtle',
|
|
206
|
+
display: 'grid',
|
|
207
|
+
gap: 1,
|
|
208
|
+
}, children: [_jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold' }, children: "Your own rules" }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Narrowing the defaults for your own agents \u2014 approval by tool, a daily credits cap, a denylist, the environments they may use \u2014 is edited here once the policy layers ship. When your agents act inside an organization, that organization's policy applies instead of yours." }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold', mt: 2 }, children: "Organizations you own" }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "The policy form for each organization you own, with its history and a preview of what one agent would get, is the same form the organization's MCP tab carries. It joins this page with the layers." })] })] }));
|
|
209
|
+
};
|
|
210
|
+
export default Policies;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fields of a policy layer, and the reading of them a layer cannot get
|
|
3
|
+
* wrong twice.
|
|
4
|
+
*
|
|
5
|
+
* One component for the organization's layer and a person's own, because
|
|
6
|
+
* the rules are the same rules and two forms would be two chances to word
|
|
7
|
+
* "an empty allowlist is not an allowlist" differently — the reading that
|
|
8
|
+
* differs from the plain one is exactly the reading that must not drift.
|
|
9
|
+
*
|
|
10
|
+
* What the two callers do differ about is what a rule *means*. An
|
|
11
|
+
* organization's layer is the floor everyone in it stands on; a person's
|
|
12
|
+
* only narrows further, and cannot loosen. `notes` is how a caller says so
|
|
13
|
+
* per field, beside the field, at the moment somebody is typing into it.
|
|
14
|
+
*
|
|
15
|
+
* @module views/mcp/PolicyForm
|
|
16
|
+
*/
|
|
17
|
+
import type { JSX } from 'react';
|
|
18
|
+
import type { McpPolicyRules } from '../../api/iam/mcpPolicy';
|
|
19
|
+
/** The form's own shape: every field a string, as an input holds it. */
|
|
20
|
+
export type PolicyDraft = {
|
|
21
|
+
toolDenylist: string;
|
|
22
|
+
toolAllowlist: string;
|
|
23
|
+
allowedClients: string;
|
|
24
|
+
maxCallsPerMinute: string;
|
|
25
|
+
maxCreditsPerDay: string;
|
|
26
|
+
maxConcurrentSandboxes: string;
|
|
27
|
+
};
|
|
28
|
+
export declare const EMPTY_POLICY_DRAFT: PolicyDraft;
|
|
29
|
+
/** A stored list as the textarea shows it: one entry a line. */
|
|
30
|
+
export declare const linesOf: (value: string[] | undefined) => string;
|
|
31
|
+
/**
|
|
32
|
+
* A textarea back to a list, or `undefined` where nothing was typed.
|
|
33
|
+
*
|
|
34
|
+
* `undefined`, never `[]`. An empty array is a rule *set* to nothing, and
|
|
35
|
+
* for an allowlist that is the difference between "I have not set one" and
|
|
36
|
+
* a setting that refuses everything.
|
|
37
|
+
*/
|
|
38
|
+
export declare const listFrom: (value: string) => string[] | undefined;
|
|
39
|
+
/** A number field back to a number, or `undefined` where left blank. */
|
|
40
|
+
export declare const numberFrom: (value: string) => number | undefined;
|
|
41
|
+
export declare const textOf: (value: number | undefined) => string;
|
|
42
|
+
/** A stored layer as the form holds it. */
|
|
43
|
+
export declare const draftOf: (rules: McpPolicyRules | null | undefined) => PolicyDraft;
|
|
44
|
+
/**
|
|
45
|
+
* The draft as it will be stored, or the reason it will not be.
|
|
46
|
+
*
|
|
47
|
+
* A cap of zero is caught here rather than at the write, because IAM's
|
|
48
|
+
* refusal is correct and unhelpful: somebody who typed 0 meant "stop my
|
|
49
|
+
* agents", and the answer they need is what to do instead.
|
|
50
|
+
*/
|
|
51
|
+
export declare const rulesFrom: (draft: PolicyDraft) => McpPolicyRules | string;
|
|
52
|
+
export declare const policyChanged: (a: PolicyDraft, b: PolicyDraft) => boolean;
|
|
53
|
+
export interface PolicyFormProps {
|
|
54
|
+
draft: PolicyDraft;
|
|
55
|
+
onChange: (key: keyof PolicyDraft, value: string) => void;
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* A line drawn under one field, when this layer's reading of it needs
|
|
59
|
+
* something said — "your organization already caps this at 30", say.
|
|
60
|
+
*
|
|
61
|
+
* Beside the field rather than at the top: somebody typing 60 into a box
|
|
62
|
+
* needs to know while their hands are on it, not after they save.
|
|
63
|
+
*/
|
|
64
|
+
notes?: Partial<Record<keyof PolicyDraft, React.ReactNode>>;
|
|
65
|
+
}
|
|
66
|
+
export declare const PolicyForm: ({ draft, onChange, disabled, notes, }: PolicyFormProps) => JSX.Element;
|
|
67
|
+
export default PolicyForm;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FormControl, TextInput, Textarea, Text } from '@primer/react';
|
|
3
|
+
import { Box } from '@datalayer/primer-addons';
|
|
4
|
+
export const EMPTY_POLICY_DRAFT = {
|
|
5
|
+
toolDenylist: '',
|
|
6
|
+
toolAllowlist: '',
|
|
7
|
+
allowedClients: '',
|
|
8
|
+
maxCallsPerMinute: '',
|
|
9
|
+
maxCreditsPerDay: '',
|
|
10
|
+
maxConcurrentSandboxes: '',
|
|
11
|
+
};
|
|
12
|
+
/** A stored list as the textarea shows it: one entry a line. */
|
|
13
|
+
export const linesOf = (value) => (value ?? []).join('\n');
|
|
14
|
+
/**
|
|
15
|
+
* A textarea back to a list, or `undefined` where nothing was typed.
|
|
16
|
+
*
|
|
17
|
+
* `undefined`, never `[]`. An empty array is a rule *set* to nothing, and
|
|
18
|
+
* for an allowlist that is the difference between "I have not set one" and
|
|
19
|
+
* a setting that refuses everything.
|
|
20
|
+
*/
|
|
21
|
+
export const listFrom = (value) => {
|
|
22
|
+
const entries = value
|
|
23
|
+
.split('\n')
|
|
24
|
+
.map(entry => entry.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
return entries.length > 0 ? entries : undefined;
|
|
27
|
+
};
|
|
28
|
+
/** A number field back to a number, or `undefined` where left blank. */
|
|
29
|
+
export const numberFrom = (value) => {
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
if (!trimmed) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const parsed = Number(trimmed);
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
36
|
+
};
|
|
37
|
+
export const textOf = (value) => value === undefined || value === null ? '' : String(value);
|
|
38
|
+
/** A stored layer as the form holds it. */
|
|
39
|
+
export const draftOf = (rules) => ({
|
|
40
|
+
toolDenylist: linesOf(rules?.toolDenylist),
|
|
41
|
+
toolAllowlist: linesOf(rules?.toolAllowlist),
|
|
42
|
+
allowedClients: linesOf(rules?.allowedClients),
|
|
43
|
+
maxCallsPerMinute: textOf(rules?.maxCallsPerMinute),
|
|
44
|
+
maxCreditsPerDay: textOf(rules?.maxCreditsPerDay),
|
|
45
|
+
maxConcurrentSandboxes: textOf(rules?.maxConcurrentSandboxes),
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* The draft as it will be stored, or the reason it will not be.
|
|
49
|
+
*
|
|
50
|
+
* A cap of zero is caught here rather than at the write, because IAM's
|
|
51
|
+
* refusal is correct and unhelpful: somebody who typed 0 meant "stop my
|
|
52
|
+
* agents", and the answer they need is what to do instead.
|
|
53
|
+
*/
|
|
54
|
+
export const rulesFrom = (draft) => {
|
|
55
|
+
const numbers = [
|
|
56
|
+
['maxCallsPerMinute', 'Calls per minute'],
|
|
57
|
+
['maxCreditsPerDay', 'Credits per day'],
|
|
58
|
+
['maxConcurrentSandboxes', 'Sandboxes at once'],
|
|
59
|
+
];
|
|
60
|
+
for (const [key, label] of numbers) {
|
|
61
|
+
const typed = draft[key].trim();
|
|
62
|
+
if (!typed) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const parsed = Number(typed);
|
|
66
|
+
if (!Number.isFinite(parsed)) {
|
|
67
|
+
return `${label} must be a number, or empty for no limit.`;
|
|
68
|
+
}
|
|
69
|
+
if (parsed <= 0) {
|
|
70
|
+
return (`${label} cannot be 0. A non-positive limit reads as *no limit*, so ` +
|
|
71
|
+
'a zero written to stop your agents would lift the limit instead. ' +
|
|
72
|
+
'To stop an agent, revoke its grant or deny the tools it uses.');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
toolDenylist: listFrom(draft.toolDenylist),
|
|
77
|
+
toolAllowlist: listFrom(draft.toolAllowlist),
|
|
78
|
+
allowedClients: listFrom(draft.allowedClients),
|
|
79
|
+
maxCallsPerMinute: numberFrom(draft.maxCallsPerMinute),
|
|
80
|
+
maxCreditsPerDay: numberFrom(draft.maxCreditsPerDay),
|
|
81
|
+
maxConcurrentSandboxes: numberFrom(draft.maxConcurrentSandboxes),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
export const policyChanged = (a, b) => Object.keys(EMPTY_POLICY_DRAFT).some(key => a[key] !== b[key]);
|
|
85
|
+
const Note = ({ children, }) => children ? (_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'attention.fg', mt: 1, mb: 0 }, children: children })) : null;
|
|
86
|
+
export const PolicyForm = ({ draft, onChange, disabled = false, notes = {}, }) => (_jsxs(Box, { sx: { display: 'grid', gap: 3 }, children: [_jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Denied tools" }), _jsx(Textarea, { block: true, rows: 3, disabled: disabled, value: draft.toolDenylist, onChange: event => onChange('toolDenylist', event.target.value), placeholder: 'execute_cell\ndelete_file' }), _jsx(FormControl.Caption, { children: "One a line. A denial is added to every other layer\u2019s and can never be lifted by one." }), _jsx(Note, { children: notes.toolDenylist })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Permitted tools" }), _jsx(Textarea, { block: true, rows: 3, disabled: disabled, value: draft.toolAllowlist, onChange: event => onChange('toolAllowlist', event.target.value), placeholder: 'read_cell\nlist_notebooks' }), _jsxs(FormControl.Caption, { children: ["One a line, and everything else is refused.", ' ', _jsx("strong", { children: "Leave it empty to set no allowlist" }), " \u2014 empty means \u201CI have not set one\u201D, not \u201Cnothing is permitted\u201D, because the second would stop every agent at once."] }), _jsx(Note, { children: notes.toolAllowlist })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Admitted clients" }), _jsx(Textarea, { block: true, rows: 3, disabled: disabled, value: draft.allowedClients, onChange: event => onChange('allowedClients', event.target.value), placeholder: 'https://claude.ai/.well-known/mcp-client.json\ncursor.com' }), _jsx(FormControl.Caption, { children: "A client\u2019s document URL, or just its hostname. Empty is not an allowlist, for the same reason as above." }), _jsx(Note, { children: notes.allowedClients })] }), _jsxs(Box, { sx: {
|
|
87
|
+
display: 'grid',
|
|
88
|
+
gap: 3,
|
|
89
|
+
gridTemplateColumns: ['1fr', '1fr 1fr 1fr'],
|
|
90
|
+
}, children: [_jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Calls per minute" }), _jsx(TextInput, { block: true, type: "number", min: 1, disabled: disabled, value: draft.maxCallsPerMinute, onChange: event => onChange('maxCallsPerMinute', event.target.value) }), _jsx(FormControl.Caption, { children: "Empty for no limit." }), _jsx(Note, { children: notes.maxCallsPerMinute })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Credits per day" }), _jsx(TextInput, { block: true, type: "number", min: 1, disabled: disabled, value: draft.maxCreditsPerDay, onChange: event => onChange('maxCreditsPerDay', event.target.value) }), _jsx(FormControl.Caption, { children: "Checked before a launch, never mid-session." }), _jsx(Note, { children: notes.maxCreditsPerDay })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Sandboxes at once" }), _jsx(TextInput, { block: true, type: "number", min: 1, disabled: disabled, value: draft.maxConcurrentSandboxes, onChange: event => onChange('maxConcurrentSandboxes', event.target.value) }), _jsx(FormControl.Caption, { children: "Counted across the layer that set it." }), _jsx(Note, { children: notes.maxConcurrentSandboxes })] })] })] }));
|
|
91
|
+
export default PolicyForm;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who changed this policy, and when.
|
|
3
|
+
*
|
|
4
|
+
* The one question a policy page cannot answer from the policy itself: it
|
|
5
|
+
* shows what is in force, and says nothing about how it got that way. An
|
|
6
|
+
* administrator who finds a denylist they did not write has, without this,
|
|
7
|
+
* nowhere to ask.
|
|
8
|
+
*
|
|
9
|
+
* The rows come from the audit — IAM records `mcp.policy.set` and
|
|
10
|
+
* `mcp.policy.remove` with who, when, the scope, the subject and the
|
|
11
|
+
* version. They are read with `?method=`, narrowed to the organization,
|
|
12
|
+
* which is why that filter exists: without it these rows are reachable only
|
|
13
|
+
* by reading past every tool call in the collection.
|
|
14
|
+
*
|
|
15
|
+
* **It can be empty for a real reason.** IAM's rows only reach the
|
|
16
|
+
* collection where a deployment has configured the Solr audit sink; with the
|
|
17
|
+
* default log sink they are in a log pipeline instead. An empty drawer that
|
|
18
|
+
* implied "nobody has ever changed this" would be a lie in exactly the
|
|
19
|
+
* deployment where somebody is trying to find out, so it says which of the
|
|
20
|
+
* two it is.
|
|
21
|
+
*
|
|
22
|
+
* @module views/mcp/PolicyHistory
|
|
23
|
+
*/
|
|
24
|
+
import type { JSX } from 'react';
|
|
25
|
+
import type { McpAuditEvent } from '../../models/McpAuditEvent';
|
|
26
|
+
/** The methods IAM writes when a policy layer is written or removed. */
|
|
27
|
+
export declare const POLICY_AUDIT_METHODS: readonly ["mcp.policy.set", "mcp.policy.remove"];
|
|
28
|
+
export interface PolicyHistoryProps {
|
|
29
|
+
/** The organization whose audit this is read from. */
|
|
30
|
+
orgUid: string;
|
|
31
|
+
/**
|
|
32
|
+
* The layer, when the drawer is about one.
|
|
33
|
+
*
|
|
34
|
+
* The audit is queried by organization and method — `subject_uid` lives in
|
|
35
|
+
* `clientInfo` and is not a Solr field — so one layer's rows are picked
|
|
36
|
+
* out of the answer here. That is honest for a handful of policy changes
|
|
37
|
+
* and would not be for a busy tool-call listing.
|
|
38
|
+
*/
|
|
39
|
+
subjectUid?: string;
|
|
40
|
+
limit?: number;
|
|
41
|
+
}
|
|
42
|
+
/** What one row says about a change, in the words of the page it is on. */
|
|
43
|
+
export declare const describeChange: (event: McpAuditEvent) => string;
|
|
44
|
+
export declare const PolicyHistory: ({ orgUid, subjectUid, limit, }: PolicyHistoryProps) => JSX.Element;
|
|
45
|
+
export default PolicyHistory;
|