feat: Phase F-5+F-7 — command-sync + sub-discussion + channel-create tools
Bundles the remaining "agent does things in Fabric" surface, skipping
presence-sync (Plexum's state machine isn't 1:1 with HF status semantics)
and attachments (Plexum has no media pipeline yet) — both deferred.
internal/subdisc/ (~140 LOC + 5 tests): persistent KV at
<profile>/state/plugin-kv/fabric-sub-discussions.json
- Entry: SubChannelID, HostAgentID, HostUserID, GuestUserIDs,
HostGuide, GuestGuide, CallbackGuildNodeID, CallbackChannelID, CreatedAt
- Open / Lookup / LookupForHost (host-enforcement) / Add / Remove / All
- atomic tmp+rename persistence; corrupt file = start empty
internal/tools/tools.go — 7 new tools:
- create-chat-channel (xType=general)
- create-work-channel (xType=work)
- create-report-channel (xType=report)
- create-discussion-channel (xType=discuss)
- discussion-complete (post summary + close channel)
- create-sub-discussion (create + invite + KV store + greeting)
- close-sub-discussion (host-only; posts callback to parent + closes)
internal/fabric/client.go: CreateChannel / CloseChannel /
JoinChannel / LeaveChannel / SetChannelPurpose / Canvas CRUD /
SyncCommands (added in earlier F-4 commit; reused here)
cmd/plexum-fabric-channel-plugin/main.go:
- subdisc.Store opened from DefaultPath at init; passed into tools.Deps
- HostConfig adds commands_sync_key + sync_commands (defaults
["new","stop"]) — F-5 command-sync to every (agent,guild) pair at
init when key is set; silently skips when omitted
- syncCommandsToGuilds: best-effort PUT per guild, logs per-guild
outcome
scripts/install.sh: manifest tools[] expanded to 16 entries
(9 from F-4 + 7 new). create-channel variants share a schema shape.
Live verified:
$ plexum plugin-call create-chat-channel \
'{"agent_id":"fabrictester","guild_node_id":"test-guild2",
"name":"plexum-f7-smoke","is_public":true}'
→ "created general channel plexum-f7-smoke (id=6315e636-...)"
$ plexum plugin-call fabric-channel-list ...
→ 3 channels listed including the new one
F-6 (attachments) + F-5b (presence-sync) + F-8 (coalesce) deliberately
deferred — see DEV-NOTES.
Tests: 5 new in internal/subdisc (27 total in this repo).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ import (
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/fabric"
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/identity"
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/inbound"
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/subdisc"
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/tokens"
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/tools"
|
||||
)
|
||||
@@ -36,10 +37,14 @@ import (
|
||||
// <profile>/plugins/plexum-fabric-channel/config.json:
|
||||
//
|
||||
// {
|
||||
// "center_api_base": "http://localhost:7001/api"
|
||||
// "center_api_base": "http://localhost:7001/api",
|
||||
// "commands_sync_key": "...", // F-5: enables /command autocomplete in Fabric
|
||||
// "sync_commands": ["new","stop"] // optional override; defaults to ["new","stop"]
|
||||
// }
|
||||
type HostConfig struct {
|
||||
CenterAPIBase string `json:"center_api_base"`
|
||||
CenterAPIBase string `json:"center_api_base"`
|
||||
CommandsSyncKey string `json:"commands_sync_key,omitempty"`
|
||||
SyncCommands []string `json:"sync_commands,omitempty"`
|
||||
}
|
||||
|
||||
type fabricPlugin struct {
|
||||
@@ -155,9 +160,16 @@ func (p *fabricPlugin) Init(ctx context.Context, host plugin.HostAPI) error {
|
||||
return p.client.AgentLogin(loginCtx, entry.FabricAPIKey)
|
||||
})
|
||||
|
||||
// Sub-discussion KV (F-7); persists across restarts.
|
||||
subStore, err := subdisc.Open(subdisc.DefaultPath())
|
||||
if err != nil {
|
||||
host.Log("warn", "fabric subdisc open", map[string]any{"err": err.Error()})
|
||||
}
|
||||
|
||||
// Agent-facing tool surface (port of openclaw's tools.ts).
|
||||
p.tools = tools.New(tools.Deps{
|
||||
Client: p.client, Tokens: p.tokens, Identities: p.identities,
|
||||
SubDisc: subStore,
|
||||
})
|
||||
|
||||
host.Log("info", "fabric channel plugin initialized", map[string]any{
|
||||
@@ -173,6 +185,14 @@ func (p *fabricPlugin) Init(ctx context.Context, host plugin.HostAPI) error {
|
||||
map[string]any{"err": err.Error()})
|
||||
}
|
||||
|
||||
// F-5: push slash command catalog to every guild the bound agents
|
||||
// belong to. Cosmetic — gives Fabric frontend "/command" autocomplete.
|
||||
// Requires commands_sync_key matching the guild's
|
||||
// FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY; silently skip when unset.
|
||||
if p.cfg.CommandsSyncKey != "" {
|
||||
p.syncCommandsToGuilds(ctx)
|
||||
}
|
||||
|
||||
// Phase F-2: start the inbound supervisor in a goroutine. Lives
|
||||
// until p.inboundCancel fires (currently never — SDK has no
|
||||
// shutdown hook; subprocess kill is the only stop signal).
|
||||
@@ -353,6 +373,47 @@ func (p *fabricPlugin) sessionFor(ctx context.Context, agentID string) (*fabric.
|
||||
return p.tokens.Get(loginCtx, agentID)
|
||||
}
|
||||
|
||||
// syncCommandsToGuilds PUTs the slash-command catalog to every unique
|
||||
// (agent, guild) the plugin knows about. Best-effort — failure on any
|
||||
// one guild is logged but doesn't block other guilds.
|
||||
func (p *fabricPlugin) syncCommandsToGuilds(ctx context.Context) {
|
||||
commands := p.cfg.SyncCommands
|
||||
if len(commands) == 0 {
|
||||
commands = []string{"new", "stop"} // host-side intercepted slashes
|
||||
}
|
||||
wire := make([]any, 0, len(commands))
|
||||
for _, c := range commands {
|
||||
wire = append(wire, map[string]any{
|
||||
"name": c, "description": "Plexum host-intercepted slash: /" + c,
|
||||
})
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, b := range p.bindings {
|
||||
sess, err := p.tokens.Get(ctx, b.AgentID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, g := range sess.Guilds {
|
||||
key := b.AgentID + "/" + g.NodeID
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
tok, err := p.tokens.GuildToken(ctx, b.AgentID, g.NodeID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := p.client.SyncCommands(ctx, g.Endpoint, tok, wire, p.cfg.CommandsSyncKey); err != nil {
|
||||
p.host.Log("warn", "fabric commands sync failed",
|
||||
map[string]any{"guild": g.NodeID, "err": err.Error()})
|
||||
} else {
|
||||
p.host.Log("info", "fabric commands synced",
|
||||
map[string]any{"guild": g.NodeID, "n": len(commands)})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func errResult(msg string) plugin.ToolResult {
|
||||
return plugin.ToolResult{
|
||||
Content: []plugin.ContentBlock{{Type: "text", Text: msg}},
|
||||
|
||||
Reference in New Issue
Block a user