Implement missing GUI RFC contracts
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Snider 2026-04-15 19:47:12 +01:00
parent 85c0d294e2
commit 2343f2522a
15 changed files with 303 additions and 28 deletions

View file

@ -11,6 +11,8 @@ type QueryModels struct{}
type QuerySettings struct{}
type QuerySettingsDefaults struct{}
type QueryConversationList struct{}
type QueryConversationGet struct {

View file

@ -202,6 +202,8 @@ func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result {
return core.Result{Value: s.discoverModels(), OK: true}
case QuerySettings:
return core.Result{Value: s.loadSettings(), OK: true}
case QuerySettingsDefaults:
return core.Result{Value: DefaultSettings(), OK: true}
case QueryConversationList:
conversations, err := s.listConversationSummaries()
return core.Result{}.New(conversations, err)
@ -264,6 +266,9 @@ func (s *Service) registerActions() {
c.Action("gui.chat.settings.load", func(_ context.Context, _ core.Options) core.Result {
return core.Result{Value: s.loadSettings(), OK: true}
})
c.Action("gui.chat.settings.defaults", func(_ context.Context, _ core.Options) core.Result {
return core.Result{Value: DefaultSettings(), OK: true}
})
c.Action("gui.chat.settings.reset", func(_ context.Context, _ core.Options) core.Result {
settings := DefaultSettings()
err := s.saveSettings(settings)

View file

@ -140,3 +140,18 @@ func TestService_Good_SelectModelUpdatesConversation(t *testing.T) {
require.True(t, updated.OK)
assert.Equal(t, "lemma", updated.Value.(Conversation).Model)
}
func TestService_Good_SettingsDefaults(t *testing.T) {
c := newChatCore(t, func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(w, "data: [DONE]\n\n")
}, &mockToolExecutor{})
result := c.QUERY(QuerySettingsDefaults{})
require.True(t, result.OK)
assert.Equal(t, DefaultSettings(), result.Value.(ChatSettings))
actionResult := c.Action("gui.chat.settings.defaults").Run(context.Background(), core.Options{})
require.True(t, actionResult.OK)
assert.Equal(t, DefaultSettings(), actionResult.Value.(ChatSettings))
}

View file

@ -292,7 +292,6 @@ func (em *WSEventManager) handleMessages(conn *websocket.Conn) {
return
}
handled := true
switch msg.Action {
case "subscribe":
em.subscribe(conn, msg.ID, msg.EventTypes)
@ -301,9 +300,6 @@ func (em *WSEventManager) handleMessages(conn *websocket.Conn) {
case "list":
em.listSubscriptions(conn)
default:
handled = false
}
if !handled {
em.closeWithPolicyViolation(conn, "unknown websocket action")
return
}
@ -319,7 +315,12 @@ func (em *WSEventManager) closeWithPolicyViolation(conn *websocket.Conn, reason
}
state.writeMu.Lock()
defer state.writeMu.Unlock()
_ = conn.WriteJSON(map[string]any{
"error": reason,
"status": websocket.ClosePolicyViolation,
})
_ = conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, reason), time.Now().Add(2*time.Second))
_ = conn.Close()
}
// subscribe adds a subscription for a client.

View file

@ -173,6 +173,10 @@ func TestWSEventManager_HandleWebSocket_ClosesOnMalformedMessage(t *testing.T) {
require.NoError(t, conn.WriteMessage(websocket.TextMessage, []byte(`{"action":`)))
payload := readJSONMessage(t, conn)
assert.Equal(t, "invalid websocket message", payload["error"])
assert.Equal(t, float64(websocket.ClosePolicyViolation), payload["status"])
_, _, err := conn.ReadMessage()
require.Error(t, err)
}
@ -184,6 +188,10 @@ func TestWSEventManager_HandleWebSocket_ClosesOnUnknownAction(t *testing.T) {
require.NoError(t, conn.WriteMessage(websocket.TextMessage, []byte(`{"action":"bogus"}`)))
payload := readJSONMessage(t, conn)
assert.Equal(t, "unknown websocket action", payload["error"])
assert.Equal(t, float64(websocket.ClosePolicyViolation), payload["status"])
_, _, err := conn.ReadMessage()
require.Error(t, err)
}

View file

@ -3,9 +3,12 @@ package display
import (
"os"
"path/filepath"
"regexp"
"strings"
)
var hlcrfSlotPattern = regexp.MustCompile(`\{\{\s*slot\s+"([^"]+)"\s*\}\}`)
func (s *Service) buildHLCRFComponents(pageURL string) string {
loaded, err := s.loadManifestForOrigin(pageURL)
if err != nil || loaded == nil {
@ -33,6 +36,7 @@ func (s *Service) buildHLCRFComponents(pageURL string) string {
}
func renderHLCRFComponent(tag, templateBody string) string {
templateBody = compileHLCRFTemplate(templateBody)
return `(function(){if(customElements.get(` + quoteJS(tag) + `)){return;}const tpl=document.createElement('template');tpl.innerHTML=` +
quoteJS(templateBody) +
`;class CoreHLCRFElement extends HTMLElement{connectedCallback(){if(this.shadowRoot){return;}const root=this.attachShadow({mode:'open'});root.appendChild(tpl.content.cloneNode(true));}}customElements.define(` +
@ -40,6 +44,20 @@ func renderHLCRFComponent(tag, templateBody string) string {
`,CoreHLCRFElement);})();`
}
func compileHLCRFTemplate(templateBody string) string {
return hlcrfSlotPattern.ReplaceAllStringFunc(templateBody, func(source string) string {
match := hlcrfSlotPattern.FindStringSubmatch(source)
if len(match) < 2 {
return source
}
slotName := strings.TrimSpace(match[1])
if slotName == "" || strings.EqualFold(slotName, "default") {
return "<slot></slot>"
}
return `<slot name="` + slotName + `"></slot>`
})
}
func defaultHLCRFTag(name string) string {
name = strings.TrimSpace(strings.ToLower(name))
name = strings.TrimSuffix(name, filepath.Ext(name))

View file

@ -46,6 +46,13 @@ func TestHLCRF_BuildHLCRFComponents_Good(t *testing.T) {
assert.Contains(t, script, "core-inline")
}
func TestHLCRF_CompileHLCRFTemplate_Good(t *testing.T) {
compiled := compileHLCRFTemplate(`<section data-slot="H">{{slot "H"}}</section><main>{{ slot "L-C" }}</main>`)
assert.Contains(t, compiled, `<slot name="H"></slot>`)
assert.Contains(t, compiled, `<slot name="L-C"></slot>`)
}
func TestHLCRF_BuildHLCRFComponents_Bad(t *testing.T) {
svc := &Service{}

View file

@ -80,8 +80,12 @@ func (s *Service) resolveCoreRoute(ctx context.Context, route string, query url.
return s.resolveModelsRoute(subpath, query)
case "chat":
return s.resolveChatRoute(ctx, subpath, query)
case "agent", "wallet", "identity":
return s.resolveUnavailableCoreRoute(segment, subpath, query)
case "agent":
return s.resolveServiceBackedCoreRoute("agent", subpath, query, "agent", "core-agent")
case "wallet":
return s.resolveServiceBackedCoreRoute("wallet", subpath, query, "wallet", "blockchain", "go-blockchain")
case "identity":
return s.resolveServiceBackedCoreRoute("identity", subpath, query, "identity", "tim", "TIM")
default:
return core.Result{
Value: coreerr.E("display.resolveCoreRoute", "unknown core route: "+segment, nil),
@ -238,6 +242,61 @@ func (s *Service) resolveUnavailableCoreRoute(route, subpath string, query url.V
}
}
func (s *Service) resolveServiceBackedCoreRoute(route, subpath string, query url.Values, serviceNames ...string) core.Result {
for _, serviceName := range serviceNames {
serviceName = strings.TrimSpace(serviceName)
if serviceName == "" {
continue
}
serviceResult := s.Core().Service(serviceName)
if !serviceResult.OK {
continue
}
payload := map[string]any{
"route": route,
"service": serviceName,
"subpath": subpath,
"query": query,
"value": serviceResult.Value,
"actions": s.actionsForService(serviceName),
"services": s.Core().Services(),
}
return core.Result{
Value: map[string]any{
"content_type": "text/html",
"body": s.renderSchemeBody(route, payload),
"route": route,
"service": serviceName,
"value": serviceResult.Value,
},
OK: true,
}
}
return s.resolveUnavailableCoreRoute(route, subpath, query)
}
func (s *Service) actionsForService(serviceName string) []string {
if strings.TrimSpace(serviceName) == "" {
return nil
}
prefixes := []string{
serviceName + ".",
"core." + serviceName + ".",
"gui." + serviceName + ".",
}
actions := make([]string, 0)
for _, actionName := range s.Core().Actions() {
for _, prefix := range prefixes {
if strings.HasPrefix(actionName, prefix) {
actions = append(actions, actionName)
break
}
}
}
sort.Strings(actions)
return actions
}
func (s *Service) currentSettingsSnapshot() map[string]any {
if s.configFile != nil {
var snapshot map[string]any

View file

@ -172,6 +172,34 @@ func TestScheme_ResolveScheme_Ugly(t *testing.T) {
assert.Contains(t, searchPayload["body"].(string), "No matches found in Core storage.")
}
func TestScheme_ResolveScheme_ServiceBackedRoute_Good(t *testing.T) {
c := core.New(
core.WithService(Register(nil)),
core.WithName("wallet", func(_ *core.Core) core.Result {
return core.Result{
Value: map[string]any{
"balance": "42.0",
"address": "lthn1example",
},
OK: true,
}
}),
core.WithServiceLock(),
)
require.True(t, c.ServiceStartup(context.Background(), nil).OK)
svc := core.MustServiceFor[*Service](c, "display")
svc.registerDefaultSchemes()
result := svc.ResolveScheme(context.Background(), "core://wallet/treasury?amount=1")
require.True(t, result.OK)
payload := result.Value.(map[string]any)
assert.Equal(t, "wallet", payload["route"])
assert.Equal(t, "wallet", payload["service"])
assert.Contains(t, payload["body"].(string), "lthn1example")
assert.Contains(t, payload["body"].(string), "42.0")
}
func TestScheme_ResolveScheme_NetworkPeers_Good(t *testing.T) {
c := core.New(
core.WithService(Register(nil)),

View file

@ -10,12 +10,7 @@ import (
func (s *Service) registerSidecarActions() {
if strings.TrimSpace(core.Env("CORE_DENO_ENABLE")) != "" && s.sidecar == nil {
manager := deno.New(deno.Options{
Binary: strings.TrimSpace(core.Env("CORE_DENO_BINARY")),
Dir: strings.TrimSpace(core.Env("CORE_DENO_DIR")),
Args: splitCommandArgs(core.Env("CORE_DENO_ARGS")),
})
s.sidecar = manager
s.sidecar = s.ensureSidecar()
_, _ = s.sidecar.Start(context.Background())
}
@ -32,6 +27,17 @@ func (s *Service) registerSidecarActions() {
s.Core().Action("display.sidecar.status", func(_ context.Context, _ core.Options) core.Result {
return core.Result{Value: s.ensureSidecar().Status(), OK: true}
})
s.Core().Action("core.deno.sidecar.start", func(ctx context.Context, _ core.Options) core.Result {
status, err := s.ensureSidecar().Start(ctx)
return core.Result{}.New(status, err)
})
s.Core().Action("core.deno.sidecar.stop", func(ctx context.Context, _ core.Options) core.Result {
status, err := s.ensureSidecar().Stop(ctx)
return core.Result{}.New(status, err)
})
s.Core().Action("core.deno.sidecar.status", func(_ context.Context, _ core.Options) core.Result {
return core.Result{Value: s.ensureSidecar().Status(), OK: true}
})
}
func (s *Service) ensureSidecar() *deno.Manager {

View file

@ -6,6 +6,7 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
@ -105,6 +106,17 @@ func VerifyManifest(manifest Manifest) error {
return nil
}
func (i Installer) Verify(ctx context.Context, manifestURL string) (Manifest, error) {
manifest, err := i.FetchManifest(ctx, manifestURL)
if err != nil {
return Manifest{}, err
}
if err := VerifyManifest(manifest); err != nil {
return Manifest{}, err
}
return manifest, nil
}
func (i Installer) Install(ctx context.Context, manifest Manifest) (string, error) {
if strings.TrimSpace(i.InstallDir) == "" {
return "", errors.New("install dir is required")
@ -148,9 +160,39 @@ func (i Installer) Install(ctx context.Context, manifest Manifest) (string, erro
if output, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("git clone failed: %w: %s", err, strings.TrimSpace(string(output)))
}
if err := writeInstalledManifest(targetDir, manifest); err != nil {
return "", err
}
return targetDir, nil
}
func (i Installer) List(ctx context.Context, registryURL string) ([]Manifest, error) {
client := i.HTTPClient
if client == nil {
client = http.DefaultClient
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, registryURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("marketplace list failed: %s", resp.Status)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, maxManifestBytes+1))
if err != nil {
return nil, err
}
if len(body) > maxManifestBytes {
return nil, fmt.Errorf("marketplace list failed: payload exceeds %d bytes", maxManifestBytes)
}
return decodeManifestList(body)
}
func validateManifestName(value string) error {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
@ -198,3 +240,39 @@ func safeName(value string) string {
}
return cleaned
}
func decodeManifestList(body []byte) ([]Manifest, error) {
trimmed := strings.TrimSpace(string(body))
if trimmed == "" {
return nil, nil
}
var manifests []Manifest
if strings.HasPrefix(trimmed, "[") {
if err := json.Unmarshal(body, &manifests); err != nil {
return nil, err
}
return manifests, nil
}
var wrapped struct {
Manifests []Manifest `json:"manifests" yaml:"manifests"`
}
if err := yaml.Unmarshal(body, &wrapped); err == nil && wrapped.Manifests != nil {
return wrapped.Manifests, nil
}
if err := yaml.Unmarshal(body, &manifests); err != nil {
return nil, err
}
return manifests, nil
}
func writeInstalledManifest(targetDir string, manifest Manifest) error {
manifestDir := filepath.Join(targetDir, ".core")
if err := os.MkdirAll(manifestDir, 0o755); err != nil {
return err
}
data, err := yaml.Marshal(manifest)
if err != nil {
return err
}
return os.WriteFile(filepath.Join(manifestDir, "marketplace.yaml"), data, 0o644)
}

View file

@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func signedManifest(t *testing.T, manifest Manifest) Manifest {
@ -152,6 +153,10 @@ func TestMarketplace_Install_Good(t *testing.T) {
assert.Contains(t, string(contents), "clone")
assert.Contains(t, string(contents), "--branch")
assert.Contains(t, string(contents), "main")
installedManifest, err := os.ReadFile(filepath.Join(targetDir, ".core", "marketplace.yaml"))
require.NoError(t, err)
assert.Contains(t, string(installedManifest), "name: Core UI")
}
func TestMarketplace_Install_Bad(t *testing.T) {
@ -191,6 +196,43 @@ func TestMarketplace_Install_Ugly(t *testing.T) {
assert.Contains(t, err.Error(), "git clone failed")
}
func TestMarketplace_Verify_Good(t *testing.T) {
manifest := signedManifest(t, Manifest{
Name: "core-ui",
Version: "1.2.3",
Repository: "https://example.com/core-ui.git",
Ref: "main",
})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
data, err := yaml.Marshal(manifest)
require.NoError(t, err)
_, _ = w.Write(data)
}))
t.Cleanup(server.Close)
installer := Installer{HTTPClient: server.Client()}
verified, err := installer.Verify(context.Background(), server.URL)
require.NoError(t, err)
assert.Equal(t, manifest.Name, verified.Name)
assert.Equal(t, manifest.Ref, verified.Ref)
}
func TestMarketplace_List_Good(t *testing.T) {
manifests := []Manifest{
{Name: "core-ui", Version: "1.2.3"},
{Name: "core-chat", Version: "0.9.0"},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("manifests:\n - name: core-ui\n version: 1.2.3\n - name: core-chat\n version: 0.9.0\n"))
}))
t.Cleanup(server.Close)
installer := Installer{HTTPClient: server.Client()}
listed, err := installer.List(context.Background(), server.URL)
require.NoError(t, err)
assert.Equal(t, manifests, listed)
}
func shellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
}

View file

@ -130,12 +130,14 @@ export class ChatStateService {
}
async resetSettings(): Promise<void> {
const reset = await this.invoke('gui.chat.settings.reset');
if (reset) {
this.settings.set(reset);
if (reset.default_model) {
this.selectedModel.set(reset.default_model);
}
const defaults = await this.invoke('gui.chat.settings.defaults');
if (!defaults) {
return;
}
const saved = await this.invoke('gui.chat.settings.save', defaults);
if (saved) {
this.settings.set(saved);
this.selectedModel.set(saved.default_model || '');
}
}
@ -479,6 +481,17 @@ export class ChatStateService {
if (route === 'gui.chat.settings.load') {
return this.settings() as T;
}
if (route === 'gui.chat.settings.defaults') {
return {
temperature: 1,
top_p: 0.95,
top_k: 64,
max_tokens: 2048,
context_window: 8192,
system_prompt: 'You are a helpful assistant.',
default_model: '',
} as T;
}
if (route === 'gui.chat.settings.save') {
const settings = payload as ChatSettings;
this.settings.set(settings);
@ -488,15 +501,7 @@ export class ChatStateService {
return settings as T;
}
if (route === 'gui.chat.settings.reset') {
const defaults: ChatSettings = {
temperature: 1,
top_p: 0.95,
top_k: 64,
max_tokens: 2048,
context_window: 8192,
system_prompt: 'You are a helpful assistant.',
default_model: '',
};
const defaults = (await this.mockInvoke('gui.chat.settings.defaults')) as ChatSettings;
this.settings.set(defaults);
return defaults as T;
}

View file

@ -12,7 +12,7 @@ import { ChatSettings, ModelEntry } from './chat.types';
<header>
<strong>Inference settings</strong>
<div class="actions">
<button type="button" (click)="reset.emit()">Reset</button>
<button type="button" (click)="reset.emit()">Reset to defaults</button>
<button type="button" (click)="closed.emit()">Close</button>
</div>
</header>

View file

@ -14,6 +14,7 @@ declare global {
export interface ChatRouteMap {
'gui.chat.models': { request: void; response: ModelEntry[] };
'gui.chat.settings.defaults': { request: void; response: ChatSettings };
'gui.chat.settings.load': { request: void; response: ChatSettings };
'gui.chat.settings.save': { request: ChatSettings; response: ChatSettings };
'gui.chat.settings.reset': { request: void; response: ChatSettings };