ide/docs/superpowers/specs/2026-03-14-runtime-provider-loading-design.md
Snider abd4bfad76
Some checks failed
Security Scan / security (push) Successful in 8s
Test / test (push) Failing after 59s
docs: add runtime provider loading design spec
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>
2026-03-14 12:12:08 +00:00

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:

  1. Assign a free port (if port: 0 in manifest)
  2. Start the binary via go-process: ./cool-widget --namespace /api/v1/cool-widget --port 9901
  3. Wait for health check: GET http://localhost:9901/health
  4. Register a ProxyProvider in the API engine that reverse-proxies to that port
  5. 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:

  1. Send SIGTERM to provider process
  2. Remove proxy routes from Gin
  3. Unload JS bundle from Angular

Hot Reload (Development)

During development (core dev in a provider dir):

  1. Watch for binary changes → restart process
  2. Watch for JS changes → reload in Angular
  3. Watch for manifest changes → re-register proxy

Install / Remove

Install

core install forge.lthn.ai/someone/cool-widget
  1. Clone or download the provider repo
  2. Verify Ed25519 signature in manifest
  3. If Go source: go build -o cool-widget . in the provider dir
  4. Copy to ~/.core/providers/cool-widget/
  5. Update ~/.core/providers/registry.yaml
  6. If IDE is running: hot-load the provider (no restart needed)

Remove

core remove cool-widget
  1. Stop the provider process
  2. Remove from ~/.core/providers/cool-widget/
  3. Update ~/.core/providers/registry.yaml
  4. If IDE is running: unload the proxy + UI

Update

core update cool-widget
  1. Pull latest from git
  2. Verify new signature
  3. Rebuild if source-based
  4. Stop old process, start new
  5. 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:

  1. Read each provider's Go source
  2. Import as compiled providers (not proxied)
  3. Embed JS bundles via //go:embed
  4. Set binary name, icon, and metadata from brand config
  5. Output: single binary with everything compiled in

Same providers, two modes: proxied (plugin) or compiled (product).

Provider Binary Contract

A provider binary must:

  1. Accept --namespace flag (API route prefix)
  2. Accept --port flag (HTTP listen port)
  3. Serve GET /health{"status": "ok"}
  4. Serve its API under the namespace path
  5. Optionally accept --ws-url flag 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:

  1. Compiled providers (via DescribableGroup.Describe())
  2. Runtime providers (via their openapi.json files)

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)