Files
orca/src/main/plugins/plugin-list-projection.ts
T

172 lines
5.9 KiB
TypeScript

import {
PLUGIN_CAPABILITY_DESCRIPTIONS,
type PluginCapabilityKind
} from '../../shared/plugins/plugin-capabilities'
import { needsReconsent } from '../../shared/plugins/plugin-consent-state'
import { pluginPanelTabKey } from '../../shared/plugins/plugin-manifest'
import type { PluginLockfile } from '../../shared/plugins/plugin-install-lockfile'
import { isInvalidDiscoveredPlugin } from './plugin-discovery'
import type { PluginService } from './plugin-service'
import { listPluginVmRecipeCommands } from '../../shared/plugins/plugin-vm-recipe-artifact'
/**
* Wire projection of installed plugins for the renderer and serve RPC.
* `invalid` = unreadable/failed manifest; `pending` = awaiting (re-)consent;
* `idle` = enabled with no worker running (lazy); `restarting` = waiting for
* supervised backoff; `errored` = crashed past the budget or failed to activate.
*/
export type PluginListPanelEntry = {
id: string
title: string
icon?: string
tabKey: `plugin:${string}`
}
export type PluginListStatus =
| 'running'
| 'restarting'
| 'idle'
| 'pending'
| 'disabled'
| 'errored'
| 'invalid'
export type PluginListEntry = {
pluginKey: string
/** Opaque identity of the exact capabilities and worker tier shown for review. */
consentFingerprint: string | null
name: string
version: string
publisher: string
description?: string
status: PluginListStatus
needsReconsent: boolean
error?: string
isDev: boolean
capabilities: { kind: PluginCapabilityKind; description: string }[]
panels: PluginListPanelEntry[]
commands: { id: string; title: string }[]
hasWorker: boolean
hasSkills: boolean
vmRecipes: {
id: string
name: string
description?: string
commands: { phase: 'create' | 'suspend' | 'resume' | 'destroy'; command: string }[]
}[]
restarts: number
source?: {
kind: 'local-path' | 'git'
reference: string
resolvedCommit: string | null
contentHash: string
}
}
export function buildPluginList(service: PluginService, lock: PluginLockfile): PluginListEntry[] {
const consents = {
pluginConsents: service.options.getPluginConsents(),
disabledPlugins: service.options.getDisabledPlugins()
}
return service.getDiscovered().map((plugin, index) => {
if (isInvalidDiscoveredPlugin(plugin)) {
// Why: invalid dev paths can contain private absolute desktop paths;
// never project those as identity over desktop/serve transports.
const fallbackKey = plugin.pluginKey ?? `invalid-development-plugin-${index + 1}`
return {
pluginKey: fallbackKey,
consentFingerprint: null,
name: fallbackKey,
version: '0.0.0',
publisher: '',
status: 'invalid' as const,
needsReconsent: false,
error: plugin.error,
isDev: plugin.isDev,
capabilities: [],
panels: [],
commands: [],
hasWorker: false,
hasSkills: false,
vmRecipes: [],
restarts: 0
}
}
const activation = service.activationState(plugin)
const worker = service.workerState(plugin.pluginKey)
const activationError = service.activationError(plugin.pluginKey)
let status: PluginListStatus
if (activation === 'disabled') {
status = 'disabled'
} else if (activation === 'pending') {
status = 'pending'
} else if (worker.state === 'errored' || activationError) {
status = 'errored'
} else if (worker.state === 'restarting') {
status = 'restarting'
} else {
status = worker.state === 'running' ? 'running' : 'idle'
}
const candidateLockEntry = lock.plugins[plugin.pluginKey]
// Why: never show provenance for bytes other than the current executable
// identity. Dev overrides execute outside the immutable installed tree and
// must never inherit the shadowed install's pinned-source attribution.
const lockEntry =
candidateLockEntry &&
!plugin.isDev &&
plugin.contentHash !== null &&
candidateLockEntry.contentHash === plugin.contentHash
? candidateLockEntry
: undefined
return {
pluginKey: plugin.pluginKey,
consentFingerprint: plugin.consentFingerprint,
name: plugin.manifest.name,
version: plugin.manifest.version,
publisher: plugin.manifest.publisher,
...(plugin.manifest.description ? { description: plugin.manifest.description } : {}),
status,
needsReconsent: needsReconsent(plugin.pluginKey, plugin.consentFingerprint, consents),
...(status === 'errored'
? { error: activationError ?? 'plugin worker crashed repeatedly' }
: {}),
isDev: plugin.isDev,
capabilities: plugin.manifest.capabilities.map((capability) => ({
kind: capability.kind,
description: PLUGIN_CAPABILITY_DESCRIPTIONS[capability.kind]
})),
panels: plugin.manifest.contributes.panels.map((panel) => ({
id: panel.id,
title: panel.title,
...(panel.icon ? { icon: panel.icon } : {}),
tabKey: pluginPanelTabKey(plugin.pluginKey, panel.id)
})),
commands: plugin.manifest.contributes.commands.map((command) => ({
id: command.id,
title: command.title
})),
hasWorker: Boolean(plugin.manifest.main),
hasSkills: plugin.manifest.contributes.skills.length > 0,
vmRecipes: service.contentPacks.vmRecipes.preview(plugin.pluginKey).map(({ recipe }) => ({
id: recipe.id,
name: recipe.name,
...(recipe.description ? { description: recipe.description } : {}),
commands: listPluginVmRecipeCommands(recipe)
})),
restarts: worker.restarts,
...(lockEntry
? {
source: {
kind: lockEntry.source.kind,
reference:
lockEntry.source.kind === 'git' ? lockEntry.source.url : lockEntry.source.path,
resolvedCommit: lockEntry.resolvedCommit,
contentHash: lockEntry.contentHash
}
}
: {})
}
})
}