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:
+10
-5
@@ -323,10 +323,15 @@ export const discoverUnits = (
|
||||
addPlugin(path.join(modules, pkg), pkg);
|
||||
continue;
|
||||
}
|
||||
// Scoped convention: `@punktfunk/plugin-*` (first-party). A scoped name resolves cleanly
|
||||
// from a single registry scope-map, so a plugin can depend on `@punktfunk/host` + `effect`
|
||||
// as shared (hoisted) deps rather than bundling its own copy of each.
|
||||
if (pkg === "@punktfunk") {
|
||||
// Scoped convention: `<any scope>/plugin-*`. A scoped name resolves cleanly from a
|
||||
// registry scope-map, so a plugin can depend on `@punktfunk/host` + `effect` as shared
|
||||
// (hoisted) deps rather than bundling its own copy of each.
|
||||
//
|
||||
// ANY scope, not just `@punktfunk`: the plugin store requires catalog entries to be
|
||||
// scoped precisely so the scope can map to that entry's registry, so a third-party
|
||||
// plugin necessarily arrives as `@their-scope/plugin-*`. Limiting discovery to the
|
||||
// first-party scope would let such a plugin install and then never run.
|
||||
if (pkg.startsWith("@")) {
|
||||
try {
|
||||
for (const scoped of fs.readdirSync(path.join(modules, pkg)).sort()) {
|
||||
if (scoped.startsWith("plugin-")) {
|
||||
@@ -334,7 +339,7 @@ export const discoverUnits = (
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no @punktfunk scope dir — fine
|
||||
// not a readable scope dir — fine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user