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:
105
internal/backend/client.go
Normal file
105
internal/backend/client.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Package backend is the typed HTTP client for Dialectic.Backend's API.
|
||||
// Endpoint shapes mirror Dialectic.OpenclawPlugin/plugin/src/backend-client.ts
|
||||
// so the two plugins drop into the same backend without per-plugin
|
||||
// adapters.
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, apiKey string) *Client {
|
||||
return &Client{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
APIKey: apiKey,
|
||||
HTTP: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Get(ctx context.Context, path string) (json.RawMessage, error) {
|
||||
return c.do(ctx, http.MethodGet, path, nil)
|
||||
}
|
||||
|
||||
func (c *Client) Post(ctx context.Context, path string, body any) (json.RawMessage, error) {
|
||||
return c.do(ctx, http.MethodPost, path, body)
|
||||
}
|
||||
|
||||
func (c *Client) Put(ctx context.Context, path string, body any) (json.RawMessage, error) {
|
||||
return c.do(ctx, http.MethodPut, path, body)
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path string, body any) (json.RawMessage, error) {
|
||||
if c.APIKey == "" {
|
||||
return nil, errors.New("dialectic api key not configured — set plugins.dialectic.config.apiKey or agentKeys[<agent_id>]")
|
||||
}
|
||||
url := c.BaseURL + ensureLeadingSlash(path)
|
||||
var rdr io.Reader
|
||||
if body != nil {
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal %s %s: %w", method, path, err)
|
||||
}
|
||||
rdr = bytes.NewReader(raw)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
res, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %w", method, path, err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
raw, _ := io.ReadAll(res.Body)
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("%s %s → %d: %s",
|
||||
method, path, res.StatusCode, truncate(raw, 500))
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func ensureLeadingSlash(p string) string {
|
||||
if strings.HasPrefix(p, "/") {
|
||||
return p
|
||||
}
|
||||
return "/" + p
|
||||
}
|
||||
|
||||
func truncate(b []byte, n int) string {
|
||||
if len(b) <= n {
|
||||
return string(b)
|
||||
}
|
||||
return string(b[:n]) + "…"
|
||||
}
|
||||
|
||||
// TopicDetail is the minimal shape the signup tool needs (debate window
|
||||
// for HF pre-check). The Dialectic backend returns more fields — they
|
||||
// pass through untouched in the raw JSON the tools surface to agents.
|
||||
type TopicDetail struct {
|
||||
ID string `json:"id"`
|
||||
DebateStartAt string `json:"debate_start_at"`
|
||||
DebateEndAt string `json:"debate_end_at"`
|
||||
}
|
||||
Reference in New Issue
Block a user