Plugin ecosystem: install/remove providers without recompile. Mining namespace pattern for route proxying. Managed process lifecycle. Dynamic JS loading. Swagger aggregation. core install/remove/update CLI commands. Co-Authored-By: Virgil <virgil@lethean.io>
9.6 KiB
Runtime Provider Loading — Plugin Ecosystem
Date: 2026-03-14 Status: Approved Depends on: Service Provider Framework, SCM Provider
Problem
All providers are currently compiled into the binary. Users cannot install, remove, or update providers without rebuilding core-ide. There's no plugin ecosystem — every provider is a Go import in main.go.
Solution
Runtime provider discovery using the Mining namespace pattern. Installed
providers run as managed processes with --namespace flags. The IDE's Gin
router proxies to them. JS bundles load dynamically in Angular. No recompile.
Architecture
~/.core/providers/
├── cool-widget/
│ ├── manifest.yaml # Name, namespace, element, permissions
│ ├── cool-widget # Binary (or path to system binary)
│ ├── openapi.json # OpenAPI spec
│ └── assets/
│ └── core-cool-widget.js # Custom element bundle
├── data-viz/
│ ├── manifest.yaml
│ ├── data-viz
│ └── assets/
│ └── core-data-viz.js
└── registry.yaml # Installed providers list
Runtime Flow
core-ide (main process)
┌─────────────────────────────────┐
│ Gin Router │
│ /api/v1/scm/* → compiled in │
│ /api/v1/brain/* → compiled in │
│ /api/v1/cool-widget/* → proxy ──┼──→ :9901 (cool-widget binary)
│ /api/v1/data-viz/* → proxy ──┼──→ :9902 (data-viz binary)
│ │
│ Angular Shell │
│ <core-scm-panel> → compiled │
│ <core-cool-widget> → dynamic │
│ <core-data-viz> → dynamic │
└─────────────────────────────────┘
Manifest Format
.core/manifest.yaml (same as go-scm's manifest loader):
code: cool-widget
name: Cool Widget Dashboard
version: 1.0.0
author: someone
licence: EUPL-1.2
# Provider configuration
namespace: /api/v1/cool-widget
port: 0 # 0 = auto-assign
binary: ./cool-widget # Relative to provider dir
args: [] # Additional CLI args
# UI
element:
tag: core-cool-widget
source: ./assets/core-cool-widget.js
# Layout
layout: HCF
slots:
H: toolbar
C: dashboard
F: status
# OpenAPI spec
spec: ./openapi.json
# Permissions (for TIM sandbox — future)
permissions:
network: ["api.example.com"]
filesystem: ["~/.core/providers/cool-widget/data/"]
# Signature
sign: <ed25519 signature>
Provider Lifecycle
Discovery
On startup, the IDE scans ~/.core/providers/*/manifest.yaml:
func DiscoverProviders(dir string) ([]RuntimeProvider, error) {
entries, _ := os.ReadDir(dir)
var providers []RuntimeProvider
for _, e := range entries {
if !e.IsDir() { continue }
m, err := manifest.Load(filepath.Join(dir, e.Name()))
if err != nil { continue }
providers = append(providers, RuntimeProvider{
Dir: filepath.Join(dir, e.Name()),
Manifest: m,
})
}
return providers, nil
}
Start
For each discovered provider:
- Assign a free port (if
port: 0in manifest) - Start the binary via go-process:
./cool-widget --namespace /api/v1/cool-widget --port 9901 - Wait for health check:
GET http://localhost:9901/health - Register a
ProxyProviderin the API engine that reverse-proxies to that port - Serve the JS bundle as a static asset at
/assets/{code}.js
type RuntimeProvider struct {
Dir string
Manifest *manifest.Manifest
Process *process.Daemon
Port int
}
func (rp *RuntimeProvider) Start(engine *api.Engine, hub *ws.Hub) error {
// Start binary
rp.Port = findFreePort()
rp.Process = process.NewDaemon(process.DaemonOptions{
Command: filepath.Join(rp.Dir, rp.Manifest.Binary),
Args: append(rp.Manifest.Args, "--namespace", rp.Manifest.Namespace, "--port", strconv.Itoa(rp.Port)),
PIDFile: filepath.Join(rp.Dir, "provider.pid"),
})
rp.Process.Start()
// Wait for health
waitForHealth(rp.Port)
// Register proxy provider
proxy := provider.NewProxy(provider.ProxyConfig{
Name: rp.Manifest.Code,
BasePath: rp.Manifest.Namespace,
Upstream: fmt.Sprintf("http://127.0.0.1:%d", rp.Port),
Element: rp.Manifest.Element,
SpecFile: filepath.Join(rp.Dir, rp.Manifest.Spec),
})
engine.Register(proxy)
// Serve JS assets
engine.Router().Static("/assets/"+rp.Manifest.Code, filepath.Join(rp.Dir, "assets"))
return nil
}
Stop
On IDE quit or provider removal:
- Send SIGTERM to provider process
- Remove proxy routes from Gin
- Unload JS bundle from Angular
Hot Reload (Development)
During development (core dev in a provider dir):
- Watch for binary changes → restart process
- Watch for JS changes → reload in Angular
- Watch for manifest changes → re-register proxy
Install / Remove
Install
core install forge.lthn.ai/someone/cool-widget
- Clone or download the provider repo
- Verify Ed25519 signature in manifest
- If Go source:
go build -o cool-widget .in the provider dir - Copy to
~/.core/providers/cool-widget/ - Update
~/.core/providers/registry.yaml - If IDE is running: hot-load the provider (no restart needed)
Remove
core remove cool-widget
- Stop the provider process
- Remove from
~/.core/providers/cool-widget/ - Update
~/.core/providers/registry.yaml - If IDE is running: unload the proxy + UI
Update
core update cool-widget
- Pull latest from git
- Verify new signature
- Rebuild if source-based
- Stop old process, start new
- Reload JS bundle
Registry
~/.core/providers/registry.yaml:
version: 1
providers:
cool-widget:
installed: "2026-03-14T12:00:00Z"
version: 1.0.0
source: forge.lthn.ai/someone/cool-widget
auto_start: true
data-viz:
installed: "2026-03-14T13:00:00Z"
version: 0.2.0
source: github.com/user/data-viz
auto_start: true
Custom Binary Build
core build --brand "My Product" --include cool-widget,data-viz
Instead of runtime proxy, this compiles the selected providers directly into the binary:
- Read each provider's Go source
- Import as compiled providers (not proxied)
- Embed JS bundles via
//go:embed - Set binary name, icon, and metadata from brand config
- Output: single binary with everything compiled in
Same providers, two modes: proxied (plugin) or compiled (product).
Provider Binary Contract
A provider binary must:
- Accept
--namespaceflag (API route prefix) - Accept
--portflag (HTTP listen port) - Serve
GET /health→{"status": "ok"} - Serve its API under the namespace path
- Optionally accept
--ws-urlflag to connect to IDE's WS hub for events
The element-template already scaffolds this pattern.
Swagger Aggregation
The IDE's SpecBuilder aggregates OpenAPI specs from:
- Compiled providers (via
DescribableGroup.Describe()) - Runtime providers (via their
openapi.jsonfiles)
Merged into one spec at /swagger/doc.json. The Swagger UI shows all
providers' endpoints in one place.
Angular Dynamic Loading
Custom elements load at runtime without Angular knowing about them at build time:
// In the IDE's Angular shell
async function loadProviderElement(tag: string, scriptUrl: string) {
if (customElements.get(tag)) return; // Already loaded
const script = document.createElement('script');
script.type = 'module';
script.src = scriptUrl;
document.head.appendChild(script);
// Wait for registration
await customElements.whenDefined(tag);
}
The tray panel and IDE layout call this for each Renderable provider discovered at startup. Angular wraps the custom element in a host component for the HLCRF slot assignment.
Security
Signature Verification
All manifests must be signed (Ed25519). Unsigned providers are rejected
unless --allow-unsigned is passed (development only).
Process Isolation
Provider processes run as the current user with no special privileges. Future: TIM containers for full sandbox (filesystem + network isolation per the manifest's permissions declaration).
Network
Providers listen on 127.0.0.1 only. No external network exposure.
The IDE's Gin router is the only entry point.
Implementation Location
| Component | Package | New/Existing |
|---|---|---|
| Provider discovery | go-scm/marketplace | Extend existing |
| Process management | go-process | Existing daemon API |
| Proxy provider | core/api/pkg/provider | New: proxy.go |
| Install/remove CLI | core/cli cmd/ | New commands |
| Runtime loader | core/ide | New: runtime.go |
| JS dynamic loading | core/ide frontend/ | New: provider-loader service |
| Registry file | go-scm/marketplace | Extend existing |
Not In Scope
- TIM container sandbox (future — Phase 4 from provider framework spec)
- Provider marketplace server (git-based discovery is sufficient)
- Revenue sharing / paid providers (future — SMSG licensing)
- Angular module federation (future — current pattern is custom elements)
- Multi-language provider SDKs (future — element-template is Go-first)