Adds an internal/presence package that ticks every 30s (configurable
via presence_interval_seconds), reads each bound agent's sm.Machine
state through host.ReadAgentState, maps to Fabric's 6-status enum,
and PUTs diffs to /api/agents/:userId/presence on every guild the
agent belongs to.
Semantic mapping (the part flagged "needed" in the prior README):
idle → idle
working → on_call
busy → busy
offline → offline
exhausted/unknown reserved for backend-side fallbacks; we don't push.
Tick is mutex-guarded (avoids the upsert race the openclaw incident
called out in agent-presence.service.ts) and diff-gated so writes are
sparse. Token-cache invalidation on PUT failure handles guild JWT
rotation.
fabric.Client gains SetAgentPresence helper. README marks F-5b ✅.
F-6 attachments:
- internal/attachments/ (~145 LOC + 8 tests): per-message Downloader
that fetches Fabric attachment URLs into $TMPDIR/plexum-fabric/<msg-id>/
with the agent's guild token, sanitizing filenames + message ids
against path-traversal
- AppendFooter renders downloaded paths as a markdown footer
("Attachments:\n - /tmp/... (mime, N bytes)\n")
- Agents access via the exec MCP tool (cat / file / etc.)
internal/inbound.Supervisor:
- new Attachments *attachments.Downloader field (nil → skip with warn)
- inbound.dispatch: when message has attachments, blocking-download +
AppendFooter before emitting notification (so agent's first turn
sees the paths)
cmd/plexum-fabric-channel-plugin:
- sup.Attachments = attachments.New("") wired at init
F-8 coalesce: no-op. openclaw plugin's coalesce buffered the
text→thinking→tool→text segments openclaw emits across multiple
deliver() calls per turn. Plexum's loop.Run returns ONE final assistant
text per turn (via extractFinalText), so coalescing isn't a concern.
The channel outbound posts a single message naturally.
F-5b presence-sync: deferred. The openclaw plugin pushes HarborForge
on-call status to Fabric's per-recipient presence so the backend can
busy-discard 'announce' deliveries. Plexum's state machine has
different semantics (idle/working/busy/offline) and is per-Plexum-agent
not per-user; mapping requires more design.
README updated with the phase status table + 16-tool list.
Tests: 8 new in internal/attachments (35 total in this repo).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>