initial drop: Dialectic.PlexumPlugin v0.1

Port of Dialectic.OpenclawPlugin to the Plexum SDK. 8 dialectic_*
tools wired to Dialectic.Backend over HTTP:

  list_topics, topic_detail, list_arguments, propose_topic,
  signup, post_argument, submit_verdict, view_verdict

Differences from the OpenClaw port worth noting:

  - Per-agent API key storage: OpenClaw used secret-mgr (one entry
    per agent's keyspace). Plexum has no secret-mgr; v1 stores
    keys directly in plugin config (apiKey + agentKeys map).

  - Agent identity at tool dispatch: OpenClaw framework surfaces
    ctx.agentId; Plexum SDK doesn't yet plumb the calling agent
    through ToolPlugin.CallTool. v1 falls back to
    config.defaultAgentID — same stop-gap HarborForge.PlexumPlugin
    is on. Tracked as upstream SDK work.

  - HF on_call coverage pre-check on signup: stub that always
    returns "skipped", matching OpenClaw v1's behavior (HarborForge
    never shipped the cross-plugin coverage query). pre_validated
    is sent as false so the backend records audit honestly.
    DIALECTIC_PLUGIN_BYPASS_HF=1 env retains parity with OpenClaw.

  - Activation: lazy (no background services, unlike HarborForge's
    eager-spawn for the calendar scheduler + monitor bridge).

Backend client follows the bearer-auth contract OpenClaw's
backend-client.ts established; endpoint shapes are unchanged.
This commit is contained in:
h z
2026-06-03 11:57:24 +01:00
commit c5593e3961
11 changed files with 997 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
// plexum-dialectic-plugin — Plexum-side Dialectic plugin.
//
// Ports Dialectic.OpenclawPlugin to the Plexum SDK: 8 dialectic_* tools
// over HTTP against Dialectic.Backend. Lazy activation (no background
// services — purely tool-call driven).
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
sdkplugin "git.hangman-lab.top/hzhang/Plexum-sdk-go/plugin"
dialcfg "git.hangman-lab.top/zhi/Dialectic.PlexumPlugin/internal/config"
"git.hangman-lab.top/zhi/Dialectic.PlexumPlugin/internal/tools"
)
var Version = "0.1.0"
type dialecticPlugin struct {
host sdkplugin.HostAPI
cfg dialcfg.Resolved
deps tools.Deps
}
func (p *dialecticPlugin) Manifest() sdkplugin.Manifest {
return manifestFromDisk()
}
func (p *dialecticPlugin) Init(ctx context.Context, host sdkplugin.HostAPI) error {
p.host = host
profileRoot := os.Getenv("PLEXUM_PROFILE_ROOT")
if profileRoot == "" {
home, _ := os.UserHomeDir()
profileRoot = filepath.Join(home, ".plexum")
}
raw, err := dialcfg.Load(profileRoot)
if err != nil {
return fmt.Errorf("load dialectic config: %w", err)
}
p.cfg = dialcfg.Resolve(raw)
host.Log("info", "dialectic plugin initialized", map[string]any{
"version": Version,
"backend": p.cfg.BackendURL,
"default_agent_id": p.cfg.DefaultAgentID,
"agent_keys_count": len(p.cfg.AgentKeys),
"has_default_key": p.cfg.APIKey != "",
})
p.deps = tools.Deps{
Config: p.cfg,
Host: host,
AgentIDFromCtx: func(ctx context.Context) string {
// v1: SDK doesn't surface the calling agent on tool ctx.
// Fall back to the per-claw default. Multi-agent claws will
// need SDK ctx plumbing — tracked upstream.
return p.cfg.DefaultAgentID
},
}
return nil
}
func (p *dialecticPlugin) CallTool(ctx context.Context, name string, input json.RawMessage) (sdkplugin.ToolResult, error) {
return tools.Dispatch(ctx, p.deps, name, input)
}
func manifestFromDisk() sdkplugin.Manifest {
exe, err := os.Executable()
if err == nil {
raw, err := os.ReadFile(filepath.Join(filepath.Dir(exe), "manifest.json"))
if err == nil {
var m sdkplugin.Manifest
if err := json.Unmarshal(raw, &m); err == nil && m.Name != "" {
return m
}
}
}
return sdkplugin.Manifest{
Name: "dialectic",
Version: Version,
Activation: sdkplugin.ActivationLazy,
Executable: "plexum-dialectic-plugin",
}
}
func main() {
if err := sdkplugin.Serve(&dialecticPlugin{}); err != nil && !errors.Is(err, context.Canceled) {
fmt.Fprintf(os.Stderr, "plexum-dialectic-plugin: %v\n", err)
os.Exit(1)
}
}