Files
orca/src/main/plugins/plugin-enablement.ts
T
Neil 1b2be7dd08 feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental)
Adds Orca's experimental plugin system behind a settings flag: a
supervised kernel, declarative content packs (VM recipes, commands and
keybindings, language packs), sandboxed iframe panels, forked worker
hosts, and a Git-backed marketplace v0 with consent, provenance and
kill-list enforcement.

Theme, icon-theme and terminal-theme contributions are deferred to a
follow-up pass.
2026-07-27 00:27:26 -07:00

88 lines
3.6 KiB
TypeScript

import {
normalizePluginConsents,
normalizePluginIdList
} from '../../shared/plugins/plugin-consent-state'
import type { Store } from '../persistence'
import type { PluginService } from './plugin-service'
import type { PluginConsentRequest } from '../../shared/plugins/plugin-consent-request'
import { verifyInstructionalPluginContent } from './plugin-instructional-content-integrity'
/**
* Single write path for consent + enablement. Consent is recorded as
* (qualified key → consent fingerprint) — never a bare id — so a capability
* expansion or addition of trusted Node code requires re-consent. The desktop
* IPC handlers and the headless RPC methods both route through here.
*/
export type PluginConsentDecision = PluginConsentRequest['decision']
/** Records the user's consent-dialog answer. Approving stores the CURRENT
* consent fingerprint and clears any disable; declining disables so the plugin
* never re-prompts on later launches. */
export async function applyPluginConsent(input: {
store: Store
pluginService: PluginService
pluginKey: PluginConsentRequest['pluginKey']
reviewedFingerprint: PluginConsentRequest['reviewedFingerprint']
decision: PluginConsentRequest['decision']
originWebContentsId?: number
}): Promise<void> {
const { store, pluginService, pluginKey } = input
const plugin = pluginService.findValidPlugin(pluginKey)
if (!plugin) {
throw new Error(`cannot record consent for unknown plugin ${pluginKey}`)
}
// Why: a same-key install can change while the dialog is open; never apply a
// decision to capabilities or a worker trust tier the user did not review.
if (input.decision === 'approve' && plugin.consentFingerprint !== input.reviewedFingerprint) {
throw new Error(`plugin ${pluginKey} changed since its permissions were reviewed`)
}
if (input.decision === 'approve') {
// Why: IPC and serve callers can bypass the renderer dialog, so main must
// prove every instructional byte is still reviewable before enabling it.
await verifyInstructionalPluginContent(plugin)
}
const settings = store.getSettings()
const disabled = new Set(normalizePluginIdList(settings.disabledPlugins))
const consents = normalizePluginConsents(settings.pluginConsents)
if (input.decision === 'approve') {
consents[pluginKey] = plugin.consentFingerprint
disabled.delete(pluginKey)
} else {
disabled.add(pluginKey)
}
store.updateSettings(
{ disabledPlugins: [...disabled], pluginConsents: consents },
{ notifyListeners: true, originWebContentsId: input.originWebContentsId }
)
await pluginService.reconcileActivationState()
}
/** Enables/disables an already-consented plugin. Enabling never bypasses
* consent: with missing or stale consent the plugin stays pending and the
* caller must run the consent flow instead. */
export async function applyPluginEnablement(input: {
store: Store
pluginService: PluginService
pluginKey: string
enabled: boolean
originWebContentsId?: number
}): Promise<void> {
const { store, pluginService, pluginKey, enabled } = input
if (!pluginService.findValidPlugin(pluginKey)) {
throw new Error(`cannot change enablement for unknown plugin ${pluginKey}`)
}
const settings = store.getSettings()
const disabled = new Set(normalizePluginIdList(settings.disabledPlugins))
if (enabled) {
disabled.delete(pluginKey)
} else {
disabled.add(pluginKey)
}
store.updateSettings(
{ disabledPlugins: [...disabled] },
{ notifyListeners: true, originWebContentsId: input.originWebContentsId }
)
await pluginService.reconcileActivationState()
}