mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 08:03:14 +00:00
172 lines
5.9 KiB
TypeScript
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
|
|
}
|
|
}
|
|
: {})
|
|
}
|
|
})
|
|
}
|