Compare commits
5 commits
9bc1fa7c69
...
dcd55a434c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcd55a434c | ||
|
|
937c08d9ed | ||
|
|
496513e8ab | ||
|
|
37b50abf57 | ||
|
|
f5bf7cba81 |
9 changed files with 2721 additions and 3 deletions
|
|
@ -50,9 +50,28 @@ func renderToString(_ js.Value, args []js.Value) any {
|
|||
return layout.Render(ctx)
|
||||
}
|
||||
|
||||
// registerComponentsJS wraps buildComponentJS for the WASM→JS bridge.
|
||||
// Takes a JSON string of slot assignments, generates the WC bundle,
|
||||
// and executes it in the browser via the Function constructor.
|
||||
func registerComponentsJS(_ js.Value, args []js.Value) any {
|
||||
if len(args) < 1 {
|
||||
return js.ValueOf("error: slotsJSON argument required")
|
||||
}
|
||||
jsCode, err := buildComponentJS(args[0].String())
|
||||
if err != nil {
|
||||
return js.ValueOf("error: " + err.Error())
|
||||
}
|
||||
// Execute the generated WC definitions in the browser context.
|
||||
// Uses the standard Function constructor — the normal Go WASM→JS pattern.
|
||||
fn := js.Global().Call("Function", jsCode)
|
||||
fn.Invoke()
|
||||
return js.ValueOf(jsCode)
|
||||
}
|
||||
|
||||
func main() {
|
||||
js.Global().Set("gohtml", js.ValueOf(map[string]any{
|
||||
"renderToString": js.FuncOf(renderToString),
|
||||
"renderToString": js.FuncOf(renderToString),
|
||||
"registerComponents": js.FuncOf(registerComponentsJS),
|
||||
}))
|
||||
|
||||
select {}
|
||||
|
|
|
|||
18
cmd/wasm/register.go
Normal file
18
cmd/wasm/register.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-html/codegen"
|
||||
)
|
||||
|
||||
// buildComponentJS takes a JSON slot map and returns the WC bundle JS string.
|
||||
// This is the pure-Go part testable without WASM.
|
||||
func buildComponentJS(slotsJSON string) (string, error) {
|
||||
var slots map[string]string
|
||||
if err := json.Unmarshal([]byte(slotsJSON), &slots); err != nil {
|
||||
return "", fmt.Errorf("registerComponents: %w", err)
|
||||
}
|
||||
return codegen.GenerateBundle(slots)
|
||||
}
|
||||
24
cmd/wasm/register_test.go
Normal file
24
cmd/wasm/register_test.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//go:build !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildComponentJS_Good(t *testing.T) {
|
||||
slotsJSON := `{"H":"nav-bar","C":"main-content"}`
|
||||
js, err := buildComponentJS(slotsJSON)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, js, "NavBar")
|
||||
assert.Contains(t, js, "MainContent")
|
||||
assert.Contains(t, js, "customElements.define")
|
||||
}
|
||||
|
||||
func TestBuildComponentJS_Bad_InvalidJSON(t *testing.T) {
|
||||
_, err := buildComponentJS("not json")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
90
codegen/codegen.go
Normal file
90
codegen/codegen.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// wcTemplate is the Web Component class template.
|
||||
// Uses closed Shadow DOM for isolation. Content is set via the shadow root's
|
||||
// DOM API using trusted go-html codegen output (never user input).
|
||||
var wcTemplate = template.Must(template.New("wc").Parse(`class {{.ClassName}} extends HTMLElement {
|
||||
#shadow;
|
||||
constructor() {
|
||||
super();
|
||||
this.#shadow = this.attachShadow({ mode: "closed" });
|
||||
}
|
||||
connectedCallback() {
|
||||
this.#shadow.textContent = "";
|
||||
const slot = this.getAttribute("data-slot") || "{{.Slot}}";
|
||||
this.dispatchEvent(new CustomEvent("wc-ready", { detail: { tag: "{{.Tag}}", slot } }));
|
||||
}
|
||||
render(html) {
|
||||
const tpl = document.createElement("template");
|
||||
tpl.insertAdjacentHTML("afterbegin", html);
|
||||
this.#shadow.textContent = "";
|
||||
this.#shadow.appendChild(tpl.content.cloneNode(true));
|
||||
}
|
||||
}`))
|
||||
|
||||
// GenerateClass produces a JS class definition for a custom element.
|
||||
func GenerateClass(tag, slot string) (string, error) {
|
||||
if !strings.Contains(tag, "-") {
|
||||
return "", fmt.Errorf("codegen: custom element tag %q must contain a hyphen", tag)
|
||||
}
|
||||
var b strings.Builder
|
||||
err := wcTemplate.Execute(&b, struct {
|
||||
ClassName, Tag, Slot string
|
||||
}{
|
||||
ClassName: TagToClassName(tag),
|
||||
Tag: tag,
|
||||
Slot: slot,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("codegen: template exec: %w", err)
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// GenerateRegistration produces the customElements.define() call.
|
||||
func GenerateRegistration(tag, className string) string {
|
||||
return fmt.Sprintf(`customElements.define("%s", %s);`, tag, className)
|
||||
}
|
||||
|
||||
// TagToClassName converts a kebab-case tag to PascalCase class name.
|
||||
func TagToClassName(tag string) string {
|
||||
parts := strings.Split(tag, "-")
|
||||
var b strings.Builder
|
||||
for _, p := range parts {
|
||||
if len(p) > 0 {
|
||||
b.WriteString(strings.ToUpper(p[:1]))
|
||||
b.WriteString(p[1:])
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// GenerateBundle produces all WC class definitions and registrations
|
||||
// for a set of HLCRF slot assignments.
|
||||
func GenerateBundle(slots map[string]string) (string, error) {
|
||||
seen := make(map[string]bool)
|
||||
var b strings.Builder
|
||||
|
||||
for slot, tag := range slots {
|
||||
if seen[tag] {
|
||||
continue
|
||||
}
|
||||
seen[tag] = true
|
||||
|
||||
cls, err := GenerateClass(tag, slot)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b.WriteString(cls)
|
||||
b.WriteByte('\n')
|
||||
b.WriteString(GenerateRegistration(tag, TagToClassName(tag)))
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
54
codegen/codegen_test.go
Normal file
54
codegen/codegen_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package codegen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateClass_Good(t *testing.T) {
|
||||
js, err := GenerateClass("photo-grid", "C")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, js, "class PhotoGrid extends HTMLElement")
|
||||
assert.Contains(t, js, "attachShadow")
|
||||
assert.Contains(t, js, `mode: "closed"`)
|
||||
assert.Contains(t, js, "photo-grid")
|
||||
}
|
||||
|
||||
func TestGenerateClass_Bad_InvalidTag(t *testing.T) {
|
||||
_, err := GenerateClass("invalid", "C")
|
||||
assert.Error(t, err, "custom element names must contain a hyphen")
|
||||
}
|
||||
|
||||
func TestGenerateRegistration_Good(t *testing.T) {
|
||||
js := GenerateRegistration("photo-grid", "PhotoGrid")
|
||||
assert.Contains(t, js, "customElements.define")
|
||||
assert.Contains(t, js, `"photo-grid"`)
|
||||
assert.Contains(t, js, "PhotoGrid")
|
||||
}
|
||||
|
||||
func TestTagToClassName_Good(t *testing.T) {
|
||||
tests := []struct{ tag, want string }{
|
||||
{"photo-grid", "PhotoGrid"},
|
||||
{"nav-breadcrumb", "NavBreadcrumb"},
|
||||
{"my-super-widget", "MySuperWidget"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := TagToClassName(tt.tag)
|
||||
assert.Equal(t, tt.want, got, "TagToClassName(%q)", tt.tag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateBundle_Good(t *testing.T) {
|
||||
slots := map[string]string{
|
||||
"H": "nav-bar",
|
||||
"C": "main-content",
|
||||
}
|
||||
js, err := GenerateBundle(slots)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, js, "NavBar")
|
||||
assert.Contains(t, js, "MainContent")
|
||||
assert.Equal(t, 2, strings.Count(js, "extends HTMLElement"))
|
||||
}
|
||||
440
docs/plans/2026-02-17-phase4-coredeno-webcomponents-design.md
Normal file
440
docs/plans/2026-02-17-phase4-coredeno-webcomponents-design.md
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
# Phase 4: CoreDeno + Web Components
|
||||
|
||||
**Date:** 2026-02-17
|
||||
**Status:** Approved
|
||||
**Heritage:** dAppServer prototype (20 repos), Chandler/Dreaming in Code
|
||||
|
||||
## Vision
|
||||
|
||||
A universal application framework where `.core/view.yml` defines what an app IS.
|
||||
Run `core` in any directory — it discovers the manifest, verifies its signature,
|
||||
and boots the application. Like `docker-compose.yml` but for applications.
|
||||
|
||||
Philosophical lineage: Mitch Kapor's Chandler (universal configurable app),
|
||||
rebuilt with Web Components, Deno sandboxing, WASM rendering, and LEM ethics.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ WebView2 (Browser) │
|
||||
│ ┌───────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Angular │ │ Web Comp │ │ go-html │ │
|
||||
│ │ (shell) │ │ (modules)│ │ WASM │ │
|
||||
│ └─────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||
│ └──────┬───────┘ │ │
|
||||
│ │ fetch/WS │ │
|
||||
└───────────────┼─────────────────────┼───────┘
|
||||
│ │
|
||||
┌───────────────┼─────────────────────┼───────┐
|
||||
│ CoreDeno (Deno sidecar) │ │
|
||||
│ ┌────────────┴──────────┐ ┌─────┴─────┐ │
|
||||
│ │ Module Loader │ │ ITW3→WC │ │
|
||||
│ │ + Permission Gates │ │ Codegen │ │
|
||||
│ │ + Dev Server (HMR) │ │ │ │
|
||||
│ └────────────┬──────────┘ └───────────┘ │
|
||||
│ │ gRPC / Unix socket │
|
||||
└───────────────┼─────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼─────────────────────────────┐
|
||||
│ Go Backend (CoreGO) │
|
||||
│ ┌────────┐ ┌┴───────┐ ┌─────────────────┐ │
|
||||
│ │ Module │ │ gRPC │ │ MCPBridge │ │
|
||||
│ │Registry│ │ Server │ │ (WebView tools) │ │
|
||||
│ └────────┘ └────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Three processes:
|
||||
- **WebView2**: Angular shell (gradual migration) + Web Components + go-html WASM
|
||||
- **CoreDeno**: Deno sidecar — module sandbox, I/O fortress, TypeScript toolchain
|
||||
- **CoreGO**: Framework backbone — lifecycle, services, I/O (core/pkg/io), gRPC server
|
||||
|
||||
## Responsibility Split
|
||||
|
||||
| Layer | Role |
|
||||
|-------|------|
|
||||
| **CoreGO** | Framework (lifecycle, services, I/O via core/pkg/io, module registry, gRPC server) |
|
||||
| **go-html** | Web Component factory (layout → Shadow DOM, manifest → custom element, WASM client-side registration) |
|
||||
| **CoreDeno** | Sandbox + toolchain (Deno permissions, TypeScript compilation, dev server, asset serving) |
|
||||
| **MCPBridge** | Retained for direct WebView tools (window control, display, clipboard, dialogs) |
|
||||
|
||||
## CoreDeno Sidecar
|
||||
|
||||
### Lifecycle
|
||||
Go spawns Deno as a managed child process at app startup. Auto-restart on crash.
|
||||
SIGTERM on app shutdown.
|
||||
|
||||
### Communication
|
||||
- **Channel**: Unix domain socket at `$XDG_RUNTIME_DIR/core/deno.sock`
|
||||
- **Protocol**: gRPC (proto definitions in `pkg/coredeno/proto/`)
|
||||
- **Direction**: Bidirectional
|
||||
- Deno → Go: I/O requests (file, network, process) gated by permissions
|
||||
- Go → Deno: Module lifecycle events, HLCRF re-render triggers
|
||||
|
||||
### Deno's Three Roles
|
||||
|
||||
**1. Module loader + sandbox**: Reads ITW3 manifests, loads modules with
|
||||
per-module `--allow-*` permission flags. Modules run in Deno's isolate.
|
||||
|
||||
**2. I/O fortress gateway**: All file/network/process I/O routed through Deno's
|
||||
permission gates before reaching Go via gRPC. A module requesting access outside
|
||||
its declared paths is denied before Go ever sees the request.
|
||||
|
||||
**3. Build/dev toolchain**: TypeScript compilation, module resolution, dev server
|
||||
with HMR. Replaces Node/npm entirely. In production, pre-compiled bundles
|
||||
embedded in binary.
|
||||
|
||||
### Permission Model
|
||||
Each module declares required permissions in its manifest:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
read: ["/data/mining/"]
|
||||
write: ["/data/mining/config.json"]
|
||||
net: ["pool.lthn.io:3333"]
|
||||
run: ["xmrig"]
|
||||
```
|
||||
|
||||
CoreDeno enforces these at the gRPC boundary.
|
||||
|
||||
## The .core/ Convention
|
||||
|
||||
### Auto-Discovery
|
||||
Run `core` in any directory. If `.core/view.yml` exists, CoreGO reads it,
|
||||
validates the ed25519 signature, and boots the application context.
|
||||
|
||||
### view.yml Format (successor to .itw3.json)
|
||||
|
||||
```yaml
|
||||
code: photo-browser
|
||||
name: Photo Browser
|
||||
version: 0.1.0
|
||||
sign: <ed25519 signature>
|
||||
|
||||
layout: HLCRF
|
||||
slots:
|
||||
H: nav-breadcrumb
|
||||
L: folder-tree
|
||||
C: photo-grid
|
||||
R: metadata-panel
|
||||
F: status-bar
|
||||
|
||||
permissions:
|
||||
read: ["./photos/"]
|
||||
net: []
|
||||
run: []
|
||||
|
||||
modules:
|
||||
- core/media
|
||||
- core/fs
|
||||
```
|
||||
|
||||
### Signed Application Loading
|
||||
The `sign` field contains an ed25519 signature. CoreGO verifies before loading.
|
||||
Unsigned or tampered manifests are rejected. The I/O fortress operates at the
|
||||
application boundary — the entire app load chain is authenticated.
|
||||
|
||||
## Web Component Lifecycle
|
||||
|
||||
1. **Discovery** → `core` reads `.core/view.yml`, verifies signature
|
||||
2. **Resolve** → CoreGO checks module registry for declared components
|
||||
3. **Codegen** → go-html generates Web Component class definitions from manifest
|
||||
4. **Permission binding** → CoreDeno wraps component I/O calls with per-module gates
|
||||
5. **Composition** → HLCRF layout assembles slots, each a custom element with Shadow DOM
|
||||
6. **Hot reload** → Dev mode: Deno watches files, WASM re-renders affected slots only
|
||||
|
||||
### HLCRF Slot Composition
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ <nav-breadcrumb> (H - shadow) │
|
||||
├────────┬───────────────┬─────────┤
|
||||
│ <folder│ <photo-grid> │<metadata│
|
||||
│ -tree> │ (C-shadow) │ -panel> │
|
||||
│(L-shad)│ │(R-shad) │
|
||||
├────────┴───────────────┴─────────┤
|
||||
│ <status-bar> (F - shadow) │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
Each slot is a custom element with closed Shadow DOM. Isolation by design —
|
||||
one module cannot reach into another's shadow tree.
|
||||
|
||||
### go-html WASM Integration
|
||||
- **Server-side (Go)**: go-html reads manifests, generates WC class definitions
|
||||
- **Client-side (WASM)**: go-html WASM in browser dynamically registers custom
|
||||
elements at runtime via `customElements.define()`
|
||||
- Same code, two targets. Server pre-renders for initial load, client handles
|
||||
dynamic re-renders when slots change.
|
||||
|
||||
## Angular Migration Path
|
||||
|
||||
**Phase 4a** (current): Web Components load inside Angular's `<router-outlet>`.
|
||||
Angular sees custom elements via `CUSTOM_ELEMENTS_SCHEMA`. No Angular code needed
|
||||
for new modules.
|
||||
|
||||
**Phase 4b**: ApplicationFrame becomes a go-html Web Component (HLCRF outer frame).
|
||||
Angular router replaced by lightweight hash-based router mapping URLs to
|
||||
`.core/view.yml` slot configurations.
|
||||
|
||||
**Phase 4c**: Angular removed. WebView2 loads:
|
||||
1. go-html WASM (layout engine + WC factory)
|
||||
2. Thin router (~50 lines)
|
||||
3. CoreDeno-served module bundles
|
||||
4. Web Awesome (design system — already vanilla custom elements)
|
||||
|
||||
## dAppServer Heritage
|
||||
|
||||
20 repos at `github.com/dAppServer/` — the original client-side server concept
|
||||
and browser↔Go communications bridge. Extract and port, not copy.
|
||||
|
||||
### Tier 1: Extract (Core Architecture)
|
||||
|
||||
| dAppServer repo | What to extract | Phase 4 target |
|
||||
|---|---|---|
|
||||
| `server` | Port 36911 bridge, ZeroMQ IPC (pub/sub + req/rep + push/pull), air-gapped PGP auth, object store, 13 test files with RPC procedures | CoreDeno sidecar, I/O fortress, auth |
|
||||
| `dappui` | Angular→WC migration, REST+WS+Wails triple, terminal (xterm.js) | Web Component framework, MCPBridge |
|
||||
| `mod-auth` | PGP zero-knowledge auth (sign→encrypt→verify→JWT), QuasiSalt, roles | Signed manifest verification, identity |
|
||||
| `mod-io-process` | Process registry, 3-layer I/O streaming (process→ZeroMQ→WS→browser) | `core/pkg/process`, event bus |
|
||||
| `app-marketplace` | Git-as-database registry, category-as-directory, install pipeline | Module registry, `.core/view.yml` loader |
|
||||
|
||||
### Tier 2: Port (Useful Patterns)
|
||||
|
||||
| dAppServer repo | What to port | Phase 4 target |
|
||||
|---|---|---|
|
||||
| `auth-server` | Keycloak + native PGP fallback | External auth option |
|
||||
| `mod-docker` | Docker socket client, container CRUD (8 ops) | `core/pkg/process` |
|
||||
| `app-mining` | CLI Bridge (camelCase→kebab-case), Process-as-Service, API proxy | Generic CLI wrapper |
|
||||
| `app-directory-browser` | Split-pane layout, lazy tree, filesystem CRUD RPCs | `<core-file-tree>` WC |
|
||||
| `wails-build-action` | Auto-stack detection, cross-platform signing, Deno CI | Build tooling |
|
||||
|
||||
### Tier 3: Reference
|
||||
|
||||
| dAppServer repo | Value |
|
||||
|---|---|
|
||||
| `depends` | Bitcoin Core hermetic build; libmultiprocess + Cap'n Proto validates process-separation |
|
||||
| `app-utils-cyberchef` | Purest manifest-only pattern ("manifest IS the app") |
|
||||
| `devops` | Cross-compilation matrix (9 triples), ancestor of ADR-001 |
|
||||
| `pwa-native-action` | PWA→Wails native shell proof, ancestor of `core-gui` |
|
||||
| `docker-images` | C++ cross-compile layers |
|
||||
|
||||
### Tier 4: Skip
|
||||
|
||||
| dAppServer repo | Reason |
|
||||
|---|---|
|
||||
| `server-sdk-python` | Auto-generated, Go replaces |
|
||||
| `server-sdk-typescript-angular` | Auto-generated, superseded |
|
||||
| `openvpn` | Unmodified upstream fork |
|
||||
| `ansible-server-base` | Standard Ansible hardening |
|
||||
| `.github` | Org profile only |
|
||||
|
||||
## Polyfills
|
||||
|
||||
dAppServer polyfilled nothing at the browser level. The prototype ran inside
|
||||
Electron/WebView2 (Chromium), which already supports all required APIs natively:
|
||||
Custom Elements v1, Shadow DOM v1, ES Modules, `fetch`, `WebSocket`,
|
||||
`customElements.define()`, `structuredClone()`.
|
||||
|
||||
**Decision**: No polyfills needed. WebView2 is Chromium-based. The minimum
|
||||
Chromium version Wails v3 targets already supports all Web Component APIs.
|
||||
|
||||
## Object Store
|
||||
|
||||
dAppServer used a file-based JSON key-value store at `data/objects/{group}/{object}.json`.
|
||||
Six operations discovered from test files:
|
||||
|
||||
| Operation | dAppServer endpoint | Phase 4 equivalent |
|
||||
|-----------|--------------------|--------------------|
|
||||
| Get | `GET /config/object/{group}/{object}` | `store.get(group, key)` |
|
||||
| Set | `POST /config/object/{group}/{object}` | `store.set(group, key, value)` |
|
||||
| Clear | `DELETE /config/object/{group}/{object}` | `store.delete(group, key)` |
|
||||
| Count | `GET /config/object/{group}/count` | `store.count(group)` |
|
||||
| Remove group | `DELETE /config/object/{group}` | `store.deleteGroup(group)` |
|
||||
| Render template | `POST /config/render` | `store.render(template, vars)` |
|
||||
|
||||
Used for: installed app registry (`conf/installed-apps.json`), menu state
|
||||
(`conf/menu.json`), per-module config, user preferences.
|
||||
|
||||
**Decision**: Go-managed storage via gRPC. CoreGO owns persistence through
|
||||
`core/pkg/io`. Modules request storage through the I/O fortress — never
|
||||
touching the filesystem directly. SQLite backend (already a dependency in
|
||||
the blockchain layer). IndexedDB reserved for client-side cache only.
|
||||
|
||||
## Templated Config Generators
|
||||
|
||||
dAppServer's `config.render` endpoint accepted a template string + variable map
|
||||
and returned the rendered output. Used to generate configs for managed processes
|
||||
(e.g., xmrig config.json from user-selected pool/wallet parameters).
|
||||
|
||||
The pattern in Phase 4:
|
||||
1. Module declares config templates in `.core/view.yml` under a `config:` key
|
||||
2. User preferences stored in the object store
|
||||
3. CoreGO renders templates at process-start time via Go `text/template`
|
||||
4. Rendered configs written to sandboxed paths the module has `write` permission for
|
||||
|
||||
Example from mining module (camelCase→kebab-case CLI arg transformation):
|
||||
```yaml
|
||||
config:
|
||||
xmrig:
|
||||
template: conf/xmrig/config.json.tmpl
|
||||
vars:
|
||||
pool: "{{ .user.pool }}"
|
||||
wallet: "{{ .user.wallet }}"
|
||||
```
|
||||
|
||||
**Decision**: Go `text/template` in CoreGO. Templates live in the module's
|
||||
`.core/` directory. Variables come from the object store. No Deno involvement —
|
||||
config rendering is a Go-side I/O fortress operation.
|
||||
|
||||
## Git-Based Plugin Marketplace
|
||||
|
||||
### dAppServer Pattern (Extracted from `app-marketplace`)
|
||||
|
||||
A Git repository serves as the package registry. No server infrastructure needed.
|
||||
|
||||
```
|
||||
marketplace/ # Git repo
|
||||
├── index.json # Root: {version, apps[], dirs[]}
|
||||
├── miner/
|
||||
│ └── index.json # Category: {version, apps[], dirs[]}
|
||||
├── utils/
|
||||
│ └── index.json # Category: {version, apps[], dirs[]}
|
||||
└── ...
|
||||
```
|
||||
|
||||
Each `index.json` entry points to a raw `.itw3.json` URL in the plugin's own repo:
|
||||
```json
|
||||
{"code": "utils-cyberchef", "name": "CyberChef", "type": "bin",
|
||||
"pkg": "https://raw.githubusercontent.com/dAppServer/app-utils-cyberchef/main/.itw3.json"}
|
||||
```
|
||||
|
||||
Install pipeline: browse index → fetch manifest → download zip from `app.url` →
|
||||
extract → run hooks (rename, etc.) → register in object store → add menu entry.
|
||||
|
||||
### Phase 4 Evolution
|
||||
|
||||
Replace GitHub-specific URLs with Forgejo-compatible Git operations:
|
||||
|
||||
1. **Registry**: A Git repo (`host-uk/marketplace`) with category directories
|
||||
and `index.json` files. Cloned/pulled by CoreGO at startup and periodically.
|
||||
2. **Manifests**: Each module's `.core/view.yml` is the manifest (replaces `.itw3.json`).
|
||||
The marketplace index points to the module's Git repo, not a raw file URL.
|
||||
3. **Distribution**: Git clone of the module repo (not zip downloads). CoreGO
|
||||
clones into a managed modules directory with depth=1.
|
||||
4. **Verification**: ed25519 signature in `view.yml` verified before loading.
|
||||
The marketplace index includes the expected signing public key.
|
||||
5. **Install hooks**: Declared in `view.yml` under `hooks:`. Executed by CoreGO
|
||||
in the I/O fortress (rename, template render, permission grant).
|
||||
6. **Updates**: `git pull` on the module repo. Signature re-verified after pull.
|
||||
If signature fails, rollback to previous commit.
|
||||
7. **Discovery**: `core marketplace list`, `core marketplace search <query>`,
|
||||
`core marketplace install <code>`.
|
||||
|
||||
```yaml
|
||||
# marketplace/index.json
|
||||
version: 1
|
||||
modules:
|
||||
- code: utils-cyberchef
|
||||
name: CyberChef Data Toolkit
|
||||
repo: https://forge.lthn.io/host-uk/mod-cyberchef.git
|
||||
sign_key: <ed25519 public key>
|
||||
category: utils
|
||||
categories:
|
||||
- miner
|
||||
- utils
|
||||
- network
|
||||
```
|
||||
|
||||
### RPC Surface (from dAppServer test extraction)
|
||||
|
||||
| Operation | CLI | RPC |
|
||||
|-----------|-----|-----|
|
||||
| Browse | `core marketplace list` | `marketplace.list(category?)` |
|
||||
| Search | `core marketplace search <q>` | `marketplace.search(query)` |
|
||||
| Install | `core marketplace install <code>` | `marketplace.install(code)` |
|
||||
| Remove | `core marketplace remove <code>` | `marketplace.remove(code)` |
|
||||
| Installed | `core marketplace installed` | `marketplace.installed()` |
|
||||
| Update | `core marketplace update <code>` | `marketplace.update(code)` |
|
||||
| Update all | `core marketplace update` | `marketplace.updateAll()` |
|
||||
|
||||
## Complete RPC Surface (Archaeological Extraction)
|
||||
|
||||
All procedures discovered from dAppServer test files and controllers:
|
||||
|
||||
### Auth
|
||||
- `auth.create(username, password)` — PGP key generation + QuasiSalt hash
|
||||
- `auth.login(username, encryptedPayload)` — Zero-knowledge PGP verify → JWT
|
||||
- `auth.delete(username)` — Remove account
|
||||
|
||||
### Crypto
|
||||
- `crypto.pgp.generateKeyPair(name, email, passphrase)` → {publicKey, privateKey}
|
||||
- `crypto.pgp.encrypt(data, publicKey)` → encryptedData
|
||||
- `crypto.pgp.decrypt(data, privateKey, passphrase)` → plaintext
|
||||
- `crypto.pgp.sign(data, privateKey, passphrase)` → signature
|
||||
- `crypto.pgp.verify(data, signature, publicKey)` → boolean
|
||||
|
||||
### Filesystem
|
||||
- `fs.list(path, detailed?)` → FileEntry[]
|
||||
- `fs.read(path)` → content
|
||||
- `fs.write(path, content)` → boolean
|
||||
- `fs.delete(path)` → boolean
|
||||
- `fs.rename(from, to)` → boolean
|
||||
- `fs.mkdir(path)` → boolean
|
||||
- `fs.isDir(path)` → boolean
|
||||
- `fs.ensureDir(path)` → boolean
|
||||
|
||||
### Process
|
||||
- `process.run(command, args, options)` → ProcessHandle
|
||||
- `process.add(request)` → key
|
||||
- `process.start(key)` → boolean
|
||||
- `process.stop(key)` → boolean
|
||||
- `process.kill(key)` → boolean
|
||||
- `process.list()` → string[]
|
||||
- `process.get(key)` → ProcessInfo
|
||||
- `process.stdout.subscribe(key)` → stream
|
||||
- `process.stdin.write(key, data)` → void
|
||||
|
||||
### Object Store
|
||||
- `store.get(group, key)` → value
|
||||
- `store.set(group, key, value)` → void
|
||||
- `store.delete(group, key)` → void
|
||||
- `store.count(group)` → number
|
||||
- `store.deleteGroup(group)` → void
|
||||
- `store.render(template, vars)` → string
|
||||
|
||||
### IPC / Event Bus
|
||||
- `ipc.pub.subscribe(channel)` → stream
|
||||
- `ipc.pub.publish(channel, message)` → void
|
||||
- `ipc.req.send(channel, message)` → response
|
||||
- `ipc.push.send(message)` → void
|
||||
|
||||
### Marketplace
|
||||
- `marketplace.list(category?)` → ModuleEntry[]
|
||||
- `marketplace.search(query)` → ModuleEntry[]
|
||||
- `marketplace.install(code)` → boolean
|
||||
- `marketplace.remove(code)` → boolean
|
||||
- `marketplace.installed()` → InstalledModule[]
|
||||
- `marketplace.update(code)` → boolean
|
||||
|
||||
## Deliverables
|
||||
|
||||
| Component | Location | Language |
|
||||
|---|---|---|
|
||||
| CoreDeno sidecar manager | `core/pkg/coredeno/` | Go |
|
||||
| gRPC proto definitions | `core/pkg/coredeno/proto/` | Protobuf |
|
||||
| gRPC server (Go side) | `core/pkg/coredeno/server.go` | Go |
|
||||
| Deno client runtime | `core-deno/` (new repo) | TypeScript |
|
||||
| ITW3 → WC codegen | `go-html/codegen/` | Go |
|
||||
| .core/view.yml loader | `core/pkg/manifest/` | Go |
|
||||
| Manifest signing/verify | `core/pkg/manifest/sign.go` | Go |
|
||||
| WASM WC registration | `go-html/cmd/wasm/` (extend) | Go |
|
||||
|
||||
## Not In Scope (Future Phases)
|
||||
|
||||
- LEM auto-loading from signed manifests
|
||||
- Marketplace server infrastructure (Git-based registry is sufficient)
|
||||
- Offline-first sync (IndexedDB client cache)
|
||||
- Full Angular removal (Phase 4c)
|
||||
- GlusterFS distributed storage (dAppServer aspiration, not needed yet)
|
||||
- Multi-chain support (Phase 4 is Lethean-only)
|
||||
2055
docs/plans/2026-02-17-phase4-implementation.md
Normal file
2055
docs/plans/2026-02-17-phase4-implementation.md
Normal file
File diff suppressed because it is too large
Load diff
12
go.mod
12
go.mod
|
|
@ -4,6 +4,14 @@ go 1.25.5
|
|||
|
||||
replace forge.lthn.ai/core/go-i18n => ../go-i18n
|
||||
|
||||
require forge.lthn.ai/core/go-i18n v0.0.0-00010101000000-000000000000
|
||||
require (
|
||||
forge.lthn.ai/core/go-i18n v0.0.0-00010101000000-000000000000
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require golang.org/x/text v0.33.0 // indirect
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
10
go.sum
10
go.sum
|
|
@ -1,2 +1,12 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue