Files
Plexum-fabric-channel-plugin/scripts/install.sh
hzhang d6bea46d00 feat: Phase F-3 + F-4 — exp backoff + agent tool surface (batch 1)
F-3 refinements:
- internal/inbound: replace fixed 3s reconnect wait with exponential
  backoff (1s → 60s, ×2, reset when prior session lasted >30s); proxy
  for "healthy" vs "flapping" and avoids hot reconnect loops when the
  server is sick

F-4 agent tool surface (port of openclaw plugin's tools.ts):
- internal/tools/tools.go (~370 LOC): Registry binds Deps {Client,
  Tokens, Identities} and exposes 8 agent-facing tools:
    fabric-send-message     post a normal message to any channel
    fabric-send-sys-msg     post a kind=sys message (bypasses turn engine)
    fabric-channel-list     list channels visible in a guild
    fabric-guild-list       list guilds the agent is in
    fabric-message-history  paginate channel messages by seq
    fabric-channel-set-purpose  PATCH the channel's purpose
    fabric-channel          fetch metadata + members for one channel
    fabric-canvas           get/share/update/remove channel canvas
- internal/tools/contracts.go: static ToolContract list — kept in sync
  with install.sh's manifest emitter
- Every agent-scoped tool requires agent_id in input args (Plexum SDK
  doesn't propagate calling agent id through CallTool today)
- guild_node_id defaults to agent's first guild for fabric-send-message

internal/fabric/client.go: new REST methods needed by tools —
PostSystemMessage, CreateChannel, CloseChannel, JoinChannel,
LeaveChannel, SetChannelPurpose, GetCanvas, ShareCanvas, UpdateCanvas,
RemoveCanvas, SyncCommands.

cmd/plexum-fabric-channel-plugin/main.go:
- Manifest declares the tool surface via tools.New(...).Contracts()
- CallTool dispatches "send" to handleSend (outbound for channel
  manager), everything else to tools.Registry.Handler(name)

scripts/install.sh:
- Manifest tools[] now lists all 9 tools with schemas — matches what
  internal/tools/contracts.go advertises

Live verified against running Fabric stack:
  $ plexum plugin-call fabric-guild-list '{"agent_id":"fabrictester"}'
    → "guilds for agent fabrictester (1): test-guild2 @ http://localhost:7003"
  $ plexum plugin-call fabric-channel-list '{...,"guild_node_id":"test-guild2"}'
    → 2 channels listed
  $ plexum plugin-call fabric-message-history '{...,"limit":5}'
    → 5 messages with timestamps + authors

F-5+ deferred:
- create-{chat,work,report,discussion}-channel (batch 2)
- sub-discussion family (state store + 3 tools)
- presence-sync + command-sync
- attachments

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:35:39 +01:00

125 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Plexum-fabric-channel-plugin local installer.
#
# Builds + installs:
# ~/.plexum/plugins/plexum-fabric-channel/plexum-fabric-channel-plugin
# ~/.plexum/plugins/plexum-fabric-channel/manifest.json (initial; empty channels list)
# ~/.local/bin/plexum-fabric-register (CLI for binding agents)
#
# Re-runnable: rebuilds binaries; overwrites manifest only if --reset-manifest.
# Profile data (identity registry, channel configs) is never touched.
#
# Flags:
# --profile <p> Override profile root (default ~/.plexum)
# --reset-manifest Overwrite the manifest's channels list with one
# derived from current channels/*.json (matches what
# the plugin advertises at runtime). Useful after
# adding/removing channels.
set -euo pipefail
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
PROFILE_DIR="${HOME}/.plexum"
USER_BIN="${HOME}/.local/bin"
RESET_MANIFEST=0
while [[ $# -gt 0 ]]; do
case "$1" in
--profile) PROFILE_DIR="$2"; shift 2 ;;
--reset-manifest) RESET_MANIFEST=1; shift ;;
-h|--help) sed -n '2,/^set -euo/p' "$0" | sed -n '/^#/p' | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown flag: $1" >&2; exit 2 ;;
esac
done
log() { printf '\033[1;34m[fabric-install]\033[0m %s\n' "$*"; }
command -v go >/dev/null || { echo "go not found on PATH" >&2; exit 1; }
PLUGIN_DIR="${PROFILE_DIR}/plugins/plexum-fabric-channel"
mkdir -p "${PLUGIN_DIR}" "${USER_BIN}"
cd "${REPO}"
VERSION="$(git describe --tags --always 2>/dev/null || echo dev)"
LDFLAGS="-X main.Version=${VERSION}"
log "building plexum-fabric-channel-plugin (v=${VERSION})"
CGO_ENABLED=0 go build -ldflags="${LDFLAGS}" \
-o "${PLUGIN_DIR}/plexum-fabric-channel-plugin" ./cmd/plexum-fabric-channel-plugin
log "building plexum-fabric-register"
CGO_ENABLED=0 go build -ldflags="${LDFLAGS}" \
-o "${USER_BIN}/plexum-fabric-register" ./cmd/plexum-fabric-register
MANIFEST_PATH="${PLUGIN_DIR}/manifest.json"
if [[ ! -f "${MANIFEST_PATH}" || ${RESET_MANIFEST} -eq 1 ]]; then
log "writing initial manifest at ${MANIFEST_PATH}"
# Build channels[] from any channels/*.json that name our plugin.
CHANNELS_DIR="${PROFILE_DIR}/channels"
CHANNELS_JSON='[]'
if [[ -d "${CHANNELS_DIR}" ]]; then
CHANNELS_JSON=$(python3 -c "
import json, os, sys
out = []
for f in sorted(os.listdir('${CHANNELS_DIR}')):
if not f.endswith('.json'): continue
try:
d = json.load(open(os.path.join('${CHANNELS_DIR}', f)))
except Exception:
continue
if d.get('plugin') == 'plexum-fabric-channel':
out.append({'name': f[:-5], 'outboundTool': 'send'})
print(json.dumps(out))
")
fi
# Tools list: kept in sync with internal/tools/contracts.go. Each
# entry mirrors a ToolContract in allContracts.
TOOLS_JSON='[
{"name":"send","description":"Outbound channel reply — host calls this after an inbound dispatch.",
"inputSchema":{"type":"object","properties":{"channel_name":{"type":"string"},"session_id":{"type":"string"},"message":{"type":"string"}},"required":["channel_name","message"]}},
{"name":"fabric-send-message","description":"Post a normal message to a Fabric channel as the bound agent.",
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"channel_id":{"type":"string"},"content":{"type":"string"}},"required":["agent_id","channel_id","content"]}},
{"name":"fabric-send-sys-msg","description":"Post a system-kind message (not subject to turn engine / wakeup).",
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"channel_id":{"type":"string"},"content":{"type":"string"}},"required":["agent_id","guild_node_id","channel_id","content"]}},
{"name":"fabric-channel-list","description":"List channels visible to the agent in a guild.",
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"}},"required":["agent_id","guild_node_id"]}},
{"name":"fabric-guild-list","description":"List the guilds the agent belongs to.",
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"}},"required":["agent_id"]}},
{"name":"fabric-message-history","description":"Paginate channel messages by seq.",
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"channel_id":{"type":"string"},"seq_from":{"type":"integer"},"seq_to":{"type":"integer"},"limit":{"type":"integer"}},"required":["agent_id","guild_node_id","channel_id"]}},
{"name":"fabric-channel-set-purpose","description":"Update the channel'"'"'s free-form purpose text.",
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"channel_id":{"type":"string"},"purpose":{"type":"string"}},"required":["agent_id","guild_node_id","channel_id","purpose"]}},
{"name":"fabric-channel","description":"Fetch metadata + member list for a specific channel.",
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"channel_id":{"type":"string"}},"required":["agent_id","guild_node_id","channel_id"]}},
{"name":"fabric-canvas","description":"Read or write the channel canvas (op: get|share|update|remove).",
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"channel_id":{"type":"string"},"op":{"type":"string","enum":["get","share","update","remove"]},"title":{"type":"string"},"format":{"type":"string","enum":["md","html","text"]},"source":{"type":"string"}},"required":["agent_id","guild_node_id","channel_id","op"]}}
]'
cat > "${MANIFEST_PATH}" <<EOF
{
"name": "plexum-fabric-channel",
"version": "${VERSION}",
"activation": "lazy",
"executable": "plexum-fabric-channel-plugin",
"contracts": {
"channels": ${CHANNELS_JSON},
"tools": ${TOOLS_JSON}
}
}
EOF
fi
cat <<EOF
[fabric-install] done.
plugin binary: ${PLUGIN_DIR}/plexum-fabric-channel-plugin
manifest: ${MANIFEST_PATH}
register CLI: ${USER_BIN}/plexum-fabric-register
Next steps:
1. Bind agents: plexum-fabric-register --agent-id <agent> --api-key fak_...
2. Bind channels: write \`${PROFILE_DIR}/channels/<plexum-name>.json\`
(see README); re-run install.sh --reset-manifest after
adding/removing channels.
3. Allow plugin: add "plexum-fabric-channel" to \`${PROFILE_DIR}/plexum.json\`
under .plugins.allow.
4. Restart: systemctl --user restart plexum
EOF