diff --git a/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx b/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx index f8b3bd032..01639229e 100644 --- a/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx +++ b/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx @@ -38,6 +38,7 @@ import { useIsWorkspaceAdmin } from '@object-ui/auth'; import { useObjectTranslation } from '@object-ui/i18n'; import { PackageIcon } from './PackageIcon'; import { MarkdownText } from './MarkdownText'; +import { PluginDisclosure } from './PluginDisclosure'; import { MarketplaceAccessDenied } from './MarketplaceAccessDenied'; import { localizePackage } from './usePackageL10n'; import { @@ -80,6 +81,9 @@ export function MarketplacePackagePage() { const [envsError, setEnvsError] = useState(null); const [selectedEnv, setSelectedEnv] = useState(''); const [seedSampleData, setSeedSampleData] = useState(false); + // PD4: a code-bearing package requires explicit acknowledgement of its + // requested permissions before the install button is enabled. + const [acknowledged, setAcknowledged] = useState(false); const [installing, setInstalling] = useState(false); const [installResult, setInstallResult] = useState<{ ok: boolean; message: string } | null>(null); // Tracks whether the package has been installed into the current @@ -471,6 +475,8 @@ export function MarketplacePackagePage() { const loc = localizePackage(pkg as any, language); const latestVersion = pkg.latest_version?.version ?? data.versions[0]?.version ?? null; const localInstall = localInstalls.find((i) => i.manifestId === pkg.manifest_id) ?? null; + // PD4 (ADR-0025 §3.11): code-bearing packages must disclose + be acknowledged. + const containsCode = !!pkg.latest_version?.contains_code; const supportsLocal = getRuntimeConfig().features.installLocal; const primaryDisabled = !latestVersion || installingLocal || installing || (!supportsLocal && !!cloudInstalledVersion); @@ -702,7 +708,7 @@ export function MarketplacePackagePage() { - { setInstallOpen(o); if (!o) setInstallResult(null); }}> + { setInstallOpen(o); if (!o) { setInstallResult(null); setAcknowledged(false); } }}> {t('marketplace.install.dialogTitle', { name: loc.displayName || pkg.manifest_id })} @@ -772,6 +778,24 @@ export function MarketplacePackagePage() { )} + {containsCode && !envsError && ( +
+ +
+ setAcknowledged(c === true)} + /> + +
+
+ )} + {installResult && (
{installResult.message} @@ -783,7 +807,7 @@ export function MarketplacePackagePage() { {!envsError && ( diff --git a/packages/app-shell/src/console/marketplace/PluginDisclosure.tsx b/packages/app-shell/src/console/marketplace/PluginDisclosure.tsx new file mode 100644 index 000000000..5a83b8e38 --- /dev/null +++ b/packages/app-shell/src/console/marketplace/PluginDisclosure.tsx @@ -0,0 +1,127 @@ +/** + * Plugin permission disclosure (ADR-0025 PD4 §3.5 / §3.11). + * + * Rendered in the install dialog for a code-bearing package. Shows what the + * user is consenting to BEFORE install: that the package contains code, the + * trust tier it runs under, whether it is signed / marketplace-reviewed, and + * the exact structured permission set it requests. The control plane exposes + * verification STATUS only (never raw signatures); see cloud + * marketplace.projectVersion. + */ + +import { Badge } from '@object-ui/components'; +import { Boxes, CheckCircle2, Code2, FolderTree, Network, ShieldAlert, ShieldCheck, Webhook } from 'lucide-react'; +import { useObjectTranslation } from '@object-ui/i18n'; +import type { MarketplacePackageVersion } from './marketplaceApi'; + +const RUNTIME_FALLBACK: Record = { + node: 'In-process · full trust', + sandbox: 'Sandboxed', + worker: 'Out-of-process', +}; + +function PermissionGroup({ + icon: Icon, + label, + items, +}: { + icon: typeof Boxes; + label: string; + items?: string[]; +}) { + if (!items || items.length === 0) return null; + return ( +
+
+ ); +} + +export function PluginDisclosure({ version }: { version?: MarketplacePackageVersion | null }) { + const { t } = useObjectTranslation(); + if (!version?.contains_code) return null; + + const perms = version.permissions ?? {}; + const hasAny = + (perms.services?.length ?? 0) + + (perms.hooks?.length ?? 0) + + (perms.network?.length ?? 0) + + (perms.fs?.length ?? 0) > + 0; + + return ( +
+
+
+ +
+ {version.runtime && ( + + {t(`marketplace.disclosure.runtime.${version.runtime}` as any, { + defaultValue: RUNTIME_FALLBACK[version.runtime] ?? version.runtime, + })} + + )} + {version.platform_verified ? ( + + + ) : ( + + + )} + {version.signed && ( + + + )} +
+ + {hasAny ? ( +
+
+ {t('marketplace.disclosure.grantsIntro', { defaultValue: 'On install, this package will be granted:' })} +
+ + + + +
+ ) : ( +
+ {t('marketplace.disclosure.noPermissions', { defaultValue: 'Requests no special permissions.' })} +
+ )} +
+ ); +} diff --git a/packages/app-shell/src/console/marketplace/marketplaceApi.ts b/packages/app-shell/src/console/marketplace/marketplaceApi.ts index 32af9654f..a3154d7e2 100644 --- a/packages/app-shell/src/console/marketplace/marketplaceApi.ts +++ b/packages/app-shell/src/console/marketplace/marketplaceApi.ts @@ -61,6 +61,14 @@ export interface MarketplacePackageDetail extends MarketplacePackageSummary { readme?: string | null; } +/** Structured permission grants a plugin requests (ADR-0025 §3.2). */ +export interface PluginPermissions { + services?: string[]; + hooks?: string[]; + network?: string[]; + fs?: string[]; +} + export interface MarketplacePackageVersion { id: string; version: string; @@ -69,6 +77,25 @@ export interface MarketplacePackageVersion { published_at?: string | null; listing_status?: string; reviewed_at?: string | null; + + // Plugin distribution disclosure (ADR-0025 PD4 §3.11). Present only for + // code-bearing versions; the control plane exposes verification STATUS, + // never the raw signatures. + artifact_kind?: string; + /** True when this version carries executable code (a `plugin` artifact). */ + contains_code?: boolean; + /** Trust tier the plugin runs under: 'node' | 'sandbox' | 'worker'. */ + runtime?: string; + /** Dependency packaging: 'bundled' | 'manifest-deps'. */ + packaging?: string; + /** Structured permission set the plugin asks the installer to grant. */ + permissions?: PluginPermissions | null; + /** Compatibility ranges ({ platform, protocol }). */ + engines?: { platform?: string; protocol?: string } | null; + /** Whether the artifact carries a verified publisher signature. */ + signed?: boolean; + /** Whether the marketplace counter-signed (reviewed + approved) this version. */ + platform_verified?: boolean; } export interface MarketplaceListResponse {