Ports the foundation of Fabric.OpenclawPlugin to a native Plexum channel plugin (Go). F-2+ phases (socket.io inbound, wakeup gate, tools, presence, etc.) follow. Layout: internal/identity/ — fabric-identity.json registry (agent → API key) internal/fabric/ — REST client (Center auth + Guild messaging) internal/config/ — channels/<name>.json fabric extension parser cmd/plexum-fabric-register/ — agent registration CLI cmd/plexum-fabric-channel-plugin/— Plexum SDK plugin entry scripts/install.sh — build + install + manifest generator Plugin behavior (F-1): - Reads <profile>/channels/*.json, filters plugin=plexum-fabric-channel, builds (plexum-channel-name → fabric channel-id) index - Validates each bound agent's API key against Center at init (warmSessions); logs warning but doesn't refuse init on bad keys - `send` MCP tool: POST plain text to the bound Fabric channel as the agent user; selects guild endpoint+token from cached session - Manifest channels[] is generated by install.sh from current channels/*.json — re-run with --reset-manifest after adding bindings - Plugin-private config at <profile>/plugins/plexum-fabric-channel/config.json (center_api_base, default http://localhost:7001/api) Live smoke verified: - plexum-fabric-register against running Fabric Center (port 7001): validated fak_..., wrote identity file with user_id + email captured Tests: identity (5) + config (6) = 11 unit tests. F-2 will hook socket.io for inbound + wakeup gating + token refresh. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
// Package config parses the per-channel binding files at
|
|
// <profile>/channels/<name>.json. Plexum's channel.Registry already
|
|
// parses the {agent_id, plugin} core; this package additionally pulls
|
|
// out the plugin-specific `fabric` extension block describing which
|
|
// Fabric (guild, channelId) the Plexum channel name maps to.
|
|
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// PluginName is what manifest.json declares as the plugin name; we
|
|
// only consume channels/*.json entries with this value in `plugin`.
|
|
const PluginName = "plexum-fabric-channel"
|
|
|
|
// FabricBinding is one Plexum-channel ↔ Fabric-channel mapping. Built
|
|
// by Load from channels/<name>.json files.
|
|
type FabricBinding struct {
|
|
// PlexumChannelName: the basename of channels/<name>.json (no .json).
|
|
// Also matches the ChannelContract.Name the plugin manifest advertises.
|
|
PlexumChannelName string
|
|
// AgentID this channel routes to (also recorded in Plexum's channel
|
|
// registry; we re-read here for plugin-internal convenience).
|
|
AgentID string
|
|
// FabricGuildNodeID — which Fabric guild owns the channel.
|
|
FabricGuildNodeID string
|
|
// FabricChannelID — the channel id within that guild.
|
|
FabricChannelID string
|
|
}
|
|
|
|
// On-disk shape; we ignore fields outside the `fabric` block.
|
|
type wireConfig struct {
|
|
AgentID string `json:"agent_id"`
|
|
Plugin string `json:"plugin"`
|
|
Fabric struct {
|
|
GuildNodeID string `json:"guild_node_id"`
|
|
ChannelID string `json:"channel_id"`
|
|
} `json:"fabric"`
|
|
}
|
|
|
|
// Load returns all FabricBindings discovered under channelsDir. Files
|
|
// whose `plugin` field doesn't match PluginName are silently skipped
|
|
// (other channel plugins coexist in the same dir). Files with our
|
|
// plugin name but missing fabric.{guild_node_id, channel_id} are
|
|
// errors — we won't silently route nowhere.
|
|
func Load(channelsDir string) ([]FabricBinding, error) {
|
|
entries, err := os.ReadDir(channelsDir)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("fabric/config: read %s: %w", channelsDir, err)
|
|
}
|
|
var out []FabricBinding
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
|
|
continue
|
|
}
|
|
path := filepath.Join(channelsDir, e.Name())
|
|
raw, rerr := os.ReadFile(path)
|
|
if rerr != nil {
|
|
return nil, fmt.Errorf("fabric/config: read %s: %w", path, rerr)
|
|
}
|
|
var w wireConfig
|
|
if jerr := json.Unmarshal(raw, &w); jerr != nil {
|
|
return nil, fmt.Errorf("fabric/config: parse %s: %w", path, jerr)
|
|
}
|
|
if w.Plugin != PluginName {
|
|
continue
|
|
}
|
|
name := strings.TrimSuffix(e.Name(), ".json")
|
|
if w.AgentID == "" {
|
|
return nil, fmt.Errorf("fabric/config: %s missing agent_id", path)
|
|
}
|
|
if w.Fabric.GuildNodeID == "" || w.Fabric.ChannelID == "" {
|
|
return nil, fmt.Errorf("fabric/config: %s missing fabric.{guild_node_id, channel_id}", path)
|
|
}
|
|
out = append(out, FabricBinding{
|
|
PlexumChannelName: name,
|
|
AgentID: w.AgentID,
|
|
FabricGuildNodeID: w.Fabric.GuildNodeID,
|
|
FabricChannelID: w.Fabric.ChannelID,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ByFabricChannel indexes bindings by (guild_node_id, channel_id) for
|
|
// fast inbound lookup. Plugin builds this map once at startup.
|
|
type ByFabricChannel map[string]*FabricBinding
|
|
|
|
// Key composes the index key.
|
|
func Key(guildNodeID, channelID string) string {
|
|
return guildNodeID + "/" + channelID
|
|
}
|
|
|
|
// Index returns a ready-to-query ByFabricChannel.
|
|
func Index(bindings []FabricBinding) ByFabricChannel {
|
|
out := make(ByFabricChannel, len(bindings))
|
|
for i := range bindings {
|
|
b := bindings[i]
|
|
out[Key(b.FabricGuildNodeID, b.FabricChannelID)] = &b
|
|
}
|
|
return out
|
|
}
|