feat(store): plugin store host module — signed catalogs, tiered trust, install jobs
The index is the verification gate: a catalog entry pins one exact version
plus that version's tarball integrity hash, so 'verified on every release'
is a property of the data rather than a promise about process. Nothing can
express 'track latest' for a catalogued plugin.
- store/index.rs signed index parse + ed25519 verify (ring), validate-and-drop
per entry, semver minHost/advisory ranges
- store/sources.rs built-in unom source (compiled-in URL + two key slots for
rotation) + operator sources in plugin-sources.json
- store/catalog.rs https fetch with size/timeout/redirect caps, signature before
parse, last-good disk cache (stale-but-usable when offline)
- store/jobs.rs single-flight install/uninstall: registry-integrity preflight
against the pin, spawn the runner CLI with live log capture,
post-install version check with rollback, provenance record,
runner restart (discovery is startup-only)
- store/manifest.rs install provenance; absence means CLI-installed
- mgmt/store.rs 12 routes under /api/v1/store, denied to the plugin token
(a plugin that can install plugins is an escalation primitive)
Also generalizes runner discovery and listInstalled from @punktfunk/plugin-*
to ANY scope's plugin-*: catalog entries must be scoped so the scope can map
to that entry's registry, so a third-party plugin necessarily arrives under
its own scope and would otherwise install but never run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,15 @@ impl PluginRegistry {
|
||||
})
|
||||
}
|
||||
|
||||
/// The ids of live registrations, without pruning or announcing anything.
|
||||
fn live_ids(&self) -> Vec<String> {
|
||||
let map = self.inner.read().unwrap_or_else(|e| e.into_inner());
|
||||
map.iter()
|
||||
.filter(|(_, s)| s.is_live())
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Remove `id`. Returns `true` if a **live** entry existed (a clean deregister); removing an
|
||||
/// already-expired or unknown id returns `false` (nothing to announce).
|
||||
fn remove(&self, id: &str) -> bool {
|
||||
@@ -222,6 +231,16 @@ pub(crate) fn registry() -> &'static PluginRegistry {
|
||||
REG.get_or_init(PluginRegistry::new)
|
||||
}
|
||||
|
||||
/// Which plugin ids are registered right now — the plugin store's "running" column
|
||||
/// ([`super::store::list_installed`]).
|
||||
///
|
||||
/// Deliberately **not** [`list_plugins`]: that one prunes expired leases and emits
|
||||
/// `plugins.changed` for each, which is right for the nav's poll but would turn the store's own
|
||||
/// polling into an event fire-hose. This is a pure read.
|
||||
pub(crate) fn live_plugin_ids() -> Vec<String> {
|
||||
registry().live_ids()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- validation
|
||||
|
||||
/// A plugin id: `definePlugin`'s kebab-case name (`^[a-z][a-z0-9-]*$`, ≤64) — the same regex the SDK
|
||||
|
||||
Reference in New Issue
Block a user