Files
Dialectic.PlexumPlugin/internal/backend/client.go
hzhang c5593e3961 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.
2026-06-03 11:57:24 +01:00

106 lines
2.7 KiB
Go

// 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"`
}