fix(presence-sync): use /api prefix + Bearer guildAccessToken (not x-api-key)
Two layered bugs in the presence-sync loop, both causing every PUT to
fail forever in prod:
1. **Missing /api prefix.** URL was `${guildBaseUrl}/agents/<id>/presence`
but the guild backend sets a global prefix 'api' in main.ts
`setGlobalPrefix('api')`. Every other REST call in this plugin
(channel.ts channels list, fabric-client.ts postMessage, canvas)
already prepends /api/ — only presence-sync missed it. Returned 404
"Cannot PUT /agents/...".
2. **Wrong auth scheme.** Plugin sent `x-api-key: <fabricApiKey>`, but
the endpoint sits behind the global APP_GUARD = ApiKeyGuard, which
actually expects `Authorization: Bearer <guildAccessToken>` (despite
its name — confusing naming on the backend side). With /api added,
error became 401 "missing bearer token". Confirmed by `docker exec
fabric-backend-guild grep APP_GUARD /app/dist/app.module.js` and
manual curl: Bearer guild token → 200 OK.
**Fix**
- presence-sync.ts: do agent-login on demand to obtain a fresh
guildAccessToken, cache it per-agent for 13 min (under the 15-min
JWT TTL), use it as Bearer for the PUT. 401 response invalidates
the cache so the next tick re-logs-in. Pushes are gated on status
changes (rare), so the login overhead is negligible.
- inbound.ts: firstGuildEndpointByAgent → firstGuildByAgent storing
both endpoint and nodeId (presence-sync needs nodeId to pick the
right token out of guildAccessTokens[]).
- index.ts: pass FabricClient to PresenceSync constructor.
**Verified in sim**
After restart, gateway log shows `fabric: presence-sync recruiter →
idle` (200 OK), zero failed PUTs, where previously it would log a 404
every ~5s per agent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
18
dist/fabric/src/inbound.js
vendored
18
dist/fabric/src/inbound.js
vendored
@@ -228,30 +228,34 @@ export class FabricInbound {
|
||||
for (const entry of this.identity.list()) {
|
||||
if (!entry.fabricUserId)
|
||||
continue;
|
||||
const presenceGuildUrl = this.firstGuildEndpointByAgent.get(entry.agentId);
|
||||
if (!presenceGuildUrl)
|
||||
const presenceGuild = this.firstGuildByAgent.get(entry.agentId);
|
||||
if (!presenceGuild)
|
||||
continue;
|
||||
out.push({
|
||||
agentId: entry.agentId,
|
||||
fabricUserId: entry.fabricUserId,
|
||||
guildBaseUrl: presenceGuildUrl,
|
||||
guildBaseUrl: presenceGuild.endpoint,
|
||||
guildNodeId: presenceGuild.nodeId,
|
||||
fabricApiKey: entry.fabricApiKey,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Filled by connectAgent for each (agent, guild). Tracks ONLY the first
|
||||
// guild per agent (used as the presence-push target).
|
||||
firstGuildEndpointByAgent = new Map();
|
||||
// guild per agent (used as the presence-push target). Stores both
|
||||
// endpoint and nodeId — presence-sync needs both: endpoint to build
|
||||
// the URL, nodeId to pick the matching guildAccessToken from a fresh
|
||||
// agent-login response.
|
||||
firstGuildByAgent = new Map();
|
||||
async connectAgent(agentId, session) {
|
||||
const selfUserId = session.user.id;
|
||||
// First-guild capture for presence-sync push target. session.guilds is
|
||||
// already in priority order from Center; we take the first one with a
|
||||
// valid endpoint and stop. Multi-guild presence is a future concern.
|
||||
if (!this.firstGuildEndpointByAgent.has(agentId)) {
|
||||
if (!this.firstGuildByAgent.has(agentId)) {
|
||||
const firstGuild = session.guilds.find((g) => typeof g.endpoint === 'string' && g.endpoint.length > 0);
|
||||
if (firstGuild)
|
||||
this.firstGuildEndpointByAgent.set(agentId, firstGuild.endpoint);
|
||||
this.firstGuildByAgent.set(agentId, { endpoint: firstGuild.endpoint, nodeId: firstGuild.nodeId });
|
||||
}
|
||||
for (const g of session.guilds) {
|
||||
const tok = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token;
|
||||
|
||||
Reference in New Issue
Block a user