merge: reconcile sister branch claude/plan-phases-b-c-bGpoS

Resolves the parallel Phase B/C work that landed on the sister branch
while this branch was in review. Both branches independently implemented
real OTel + Wazero runtimes; this merge keeps the best of each.

Conflict resolution
- Server/plugin/sandbox_wazero.go: rewritten as a hybrid. Keeps the
  HEAD lifecycle (eager `platformInit` with WASI preview-1, explicit
  `platformDeactivate` per-instance, runtime closed via Registry.Close)
  AND adopts the sister branch's richer artefacts:
    * `WithMemoryLimitPages` actually enforces `cfg.MaxMemoryMB`,
    * the JSON-over-linear-memory ABI
      (`allocate` / `command_dispatch(ptr,len) → (ptr,len)` /
      `deallocate`),
    * `listExportedCommands` auto-binds commands the plugin exports
      via `list_commands` at activation time (capability-gated).
- Server/plugin/registry.go: kept the HEAD `activate()` snapshot
  pattern (read `runtimePlatform` under RLock, pass into
  `activateWithRuntime` as a parameter) so a concurrent Close can't
  race the wazero call. Sister branch's LoadAll stale-staging cleanup
  and UninstallPlugin on-disk dir removal came in via auto-merge.
- Server/telemetry/telemetry_otel.go: kept the HEAD implementation
  (race-fixed AppMetrics rebind, uint64 overflow guard, idempotent
  shutdown, trace-provider cleanup on prom failure) and wired in the
  sister branch's `OTLPInsecure` config field for plaintext gRPC opt-in.
- Server/go.mod: accepted sister branch's `BurntSushi/toml v1.6.0`
  for the new TOML manifest support.
- Client/tauri-client/vitest.config.ts: union of both globs
  (`tests/**/*.test.ts`, `src/**/*.test.ts`, `src/**/*.test.tsx`).
- PHASE_BC_LOCAL_TODO.md: combined the two checkbox histories;
  TOML manifest, hello.wasm fixture, and OTLPInsecure are all marked
  done now.

Sister branch additions accepted via auto-merge
- Server/plugin/examples/hello/{hello.wasm,main.go}: precompiled 925
  KiB TinyGo plugin with the full ABI (allocate, deallocate,
  list_commands, command_dispatch, on_event).
- Server/plugin/manifest_{toml,nottoml}.go: TOML manifest parser
  behind the wazero build tag, JSON fallback elsewhere.
- Server/plugin/loader.go: prefers `plugin.toml`, falls back to
  `plugin.json`.
- Server/api/plugins_handler.go: structured error responses + slog.
- Server/main.go, Server/config/config.go: OTLPInsecure plumbing,
  defaults polish.
- docs/{contributing.md,server-configuration.md}: documentation
  updates.

Test status
- `go build` passes on default, -tags otel, -tags wazero, and
  -tags otel,wazero.
- `go vet` passes on every tag combination.
- `go test ./...` passes on default and on -tags otel,wazero.
- Client: `npx tsc --noEmit` clean; vitest 3188/3188 across 112 files.

https://claude.ai/code/session_01AZni6CDSQeu67WSWY1YCDX
This commit is contained in:
Claude
2026-04-07 06:23:09 +00:00
23 changed files with 587 additions and 155 deletions
+16 -7
View File
@@ -7,6 +7,7 @@ package api
import (
"io"
"log/slog"
"net/http"
"strconv"
"strings"
@@ -54,7 +55,7 @@ func (h *PluginAdminHandler) install(w http.ResponseWriter, r *http.Request) {
// client can't tie up parsing memory.
r.Body = http.MaxBytesReader(w, r.Body, maxPluginUploadBytes+1024)
if err := r.ParseMultipartForm(maxPluginUploadBytes); err != nil {
http.Error(w, "invalid multipart upload: "+err.Error(), http.StatusBadRequest)
http.Error(w, "invalid multipart upload", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("plugin")
@@ -80,7 +81,7 @@ func (h *PluginAdminHandler) install(w http.ResponseWriter, r *http.Request) {
// for archive/zip and the cap is small enough to be safe.
body, err := io.ReadAll(io.LimitReader(file, maxPluginUploadBytes+1))
if err != nil {
http.Error(w, "read upload: "+err.Error(), http.StatusBadRequest)
http.Error(w, "failed to read upload", http.StatusBadRequest)
return
}
if int64(len(body)) > maxPluginUploadBytes {
@@ -96,7 +97,11 @@ func (h *PluginAdminHandler) install(w http.ResponseWriter, r *http.Request) {
}
name, err := h.registry.InstallFromZip(r.Context(), body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
slog.Error("plugin install failed", "error", err)
writeJSON(w, http.StatusBadRequest, errorResponse{
Error: "INSTALL_FAILED",
Message: "plugin installation failed",
})
return
}
writeJSON(w, http.StatusCreated, map[string]any{"name": name})
@@ -110,7 +115,8 @@ func (h *PluginAdminHandler) list(w http.ResponseWriter, r *http.Request) {
}
rows, err := h.store.ListPlugins(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
slog.Error("plugin list failed", "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, rows)
@@ -126,7 +132,8 @@ func (h *PluginAdminHandler) enable(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.registry.EnablePlugin(r.Context(), id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
slog.Error("plugin enable failed", "id", id, "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
@@ -142,7 +149,8 @@ func (h *PluginAdminHandler) disable(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.registry.DisablePlugin(r.Context(), id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
slog.Error("plugin disable failed", "id", id, "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
@@ -158,7 +166,8 @@ func (h *PluginAdminHandler) uninstall(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.registry.UninstallPlugin(r.Context(), id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
slog.Error("plugin uninstall failed", "id", id, "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)