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>
139 lines
9.8 KiB
Bash
Executable File
139 lines
9.8 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"]}},
|
|
{"name":"create-chat-channel","description":"Create a chat channel (xType=general).",
|
|
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"name":{"type":"string"},"is_public":{"type":"boolean"},"member_user_ids":{"type":"array","items":{"type":"string"}},"listeners":{"type":"array","items":{"type":"string"}},"on_duty":{"type":"string"},"purpose":{"type":"string"}},"required":["agent_id","guild_node_id","name"]}},
|
|
{"name":"create-work-channel","description":"Create a work channel (xType=work).",
|
|
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"name":{"type":"string"},"is_public":{"type":"boolean"},"member_user_ids":{"type":"array","items":{"type":"string"}},"listeners":{"type":"array","items":{"type":"string"}},"on_duty":{"type":"string"},"purpose":{"type":"string"}},"required":["agent_id","guild_node_id","name"]}},
|
|
{"name":"create-report-channel","description":"Create a report channel (xType=report).",
|
|
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"name":{"type":"string"},"is_public":{"type":"boolean"},"member_user_ids":{"type":"array","items":{"type":"string"}},"listeners":{"type":"array","items":{"type":"string"}},"on_duty":{"type":"string"},"purpose":{"type":"string"}},"required":["agent_id","guild_node_id","name"]}},
|
|
{"name":"create-discussion-channel","description":"Create a discussion channel (xType=discuss).",
|
|
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"name":{"type":"string"},"is_public":{"type":"boolean"},"member_user_ids":{"type":"array","items":{"type":"string"}},"listeners":{"type":"array","items":{"type":"string"}},"on_duty":{"type":"string"},"purpose":{"type":"string"}},"required":["agent_id","guild_node_id","name"]}},
|
|
{"name":"discussion-complete","description":"Post a summary then close the channel.",
|
|
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"channel_id":{"type":"string"},"summary":{"type":"string"}},"required":["agent_id","guild_node_id","channel_id","summary"]}},
|
|
{"name":"create-sub-discussion","description":"Spawn a sub-discussion channel with the caller as host + invited guests.",
|
|
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"guild_node_id":{"type":"string"},"name":{"type":"string"},"guest_user_ids":{"type":"array","items":{"type":"string"}},"host_guide":{"type":"string"},"guest_guide":{"type":"string"},"parent_channel_id":{"type":"string"},"greeting":{"type":"string"},"x_type":{"type":"string","enum":["discuss","work","report","general"]}},"required":["agent_id","guild_node_id","name","parent_channel_id"]}},
|
|
{"name":"close-sub-discussion","description":"Close a sub-discussion the caller hosts and post a callback to its parent channel.",
|
|
"inputSchema":{"type":"object","properties":{"agent_id":{"type":"string"},"sub_channel_id":{"type":"string"},"summary":{"type":"string"}},"required":["agent_id","sub_channel_id"]}}
|
|
]'
|
|
|
|
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
|