fix(plugin): refresh guild token per dispatch (fixes attachment 401)
Guild access tokens are short-lived (~15 min); the inbound socket survives via socket.io reconnect but the token captured at connect time goes stale, so attachment downloads (and reply posts) start 401ing on long-lived agents. Re-login with the agent's Fabric API key on a short TTL and use the fresh token for fetch + post. Verified live: 'fabric: fetched 1 attachment(s)' now succeeds where it previously logged 'attachment fetch 401'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
31
dist/fabric/src/inbound.js
vendored
31
dist/fabric/src/inbound.js
vendored
@@ -12,6 +12,35 @@ export class FabricInbound {
|
|||||||
accounts;
|
accounts;
|
||||||
sockets = [];
|
sockets = [];
|
||||||
seen = new Set();
|
seen = new Set();
|
||||||
|
// Guild access tokens are short-lived (~15 min). The socket survives via
|
||||||
|
// socket.io reconnect, but the token captured at connect time goes stale,
|
||||||
|
// so HTTP calls (attachment download, posting the reply) start 401ing.
|
||||||
|
// Re-login per agent on a short TTL to keep a fresh token.
|
||||||
|
tokenCache = new Map();
|
||||||
|
static TOKEN_TTL_MS = 8 * 60 * 1000;
|
||||||
|
// Return a fresh guild access token for the agent, re-authenticating with
|
||||||
|
// the agent's Fabric API key when the cached session is stale. Falls back
|
||||||
|
// to the connect-time session token if re-login fails.
|
||||||
|
async freshGuildToken(agentId, guildNodeId, fallback) {
|
||||||
|
const pick = (s) => s.guildAccessTokens.find((t) => t.guildNodeId === guildNodeId)?.token;
|
||||||
|
const now = Date.now();
|
||||||
|
const cached = this.tokenCache.get(agentId);
|
||||||
|
if (cached && now - cached.at < FabricInbound.TOKEN_TTL_MS) {
|
||||||
|
return pick(cached.session) ?? pick(fallback);
|
||||||
|
}
|
||||||
|
const apiKey = this.identity.findByAgentId(agentId)?.fabricApiKey;
|
||||||
|
if (apiKey) {
|
||||||
|
try {
|
||||||
|
const s = await this.client.agentLogin(apiKey);
|
||||||
|
this.tokenCache.set(agentId, { session: s, at: now });
|
||||||
|
return pick(s) ?? pick(fallback);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.log.warn(`fabric: token refresh failed agent=${agentId}: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pick(fallback);
|
||||||
|
}
|
||||||
constructor(core, // PluginRuntime
|
constructor(core, // PluginRuntime
|
||||||
cfg, // OpenClawConfig
|
cfg, // OpenClawConfig
|
||||||
client, identity, log, accounts = []) {
|
client, identity, log, accounts = []) {
|
||||||
@@ -151,7 +180,7 @@ export class FabricInbound {
|
|||||||
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
||||||
agentId: route.agentId,
|
agentId: route.agentId,
|
||||||
});
|
});
|
||||||
const gt = session.guildAccessTokens.find((t) => t.guildNodeId === guild.nodeId)?.token;
|
const gt = await this.freshGuildToken(agentId, guild.nodeId, session);
|
||||||
// Fetch any uploaded files for the agent: download to a temp dir and
|
// Fetch any uploaded files for the agent: download to a temp dir and
|
||||||
// hand openclaw local MediaPaths (+types) so the model receives them.
|
// hand openclaw local MediaPaths (+types) so the model receives them.
|
||||||
const media = await this.fetchAttachments(agentId, guild.endpoint, gt, m);
|
const media = await this.fetchAttachments(agentId, guild.endpoint, gt, m);
|
||||||
|
|||||||
@@ -38,6 +38,40 @@ type FabricMessage = {
|
|||||||
export class FabricInbound {
|
export class FabricInbound {
|
||||||
private sockets: Socket[] = [];
|
private sockets: Socket[] = [];
|
||||||
private seen = new Set<string>();
|
private seen = new Set<string>();
|
||||||
|
// Guild access tokens are short-lived (~15 min). The socket survives via
|
||||||
|
// socket.io reconnect, but the token captured at connect time goes stale,
|
||||||
|
// so HTTP calls (attachment download, posting the reply) start 401ing.
|
||||||
|
// Re-login per agent on a short TTL to keep a fresh token.
|
||||||
|
private tokenCache = new Map<string, { session: FabricSession; at: number }>();
|
||||||
|
private static readonly TOKEN_TTL_MS = 8 * 60 * 1000;
|
||||||
|
|
||||||
|
// Return a fresh guild access token for the agent, re-authenticating with
|
||||||
|
// the agent's Fabric API key when the cached session is stale. Falls back
|
||||||
|
// to the connect-time session token if re-login fails.
|
||||||
|
private async freshGuildToken(
|
||||||
|
agentId: string,
|
||||||
|
guildNodeId: string,
|
||||||
|
fallback: FabricSession,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const pick = (s: FabricSession) =>
|
||||||
|
s.guildAccessTokens.find((t) => t.guildNodeId === guildNodeId)?.token;
|
||||||
|
const now = Date.now();
|
||||||
|
const cached = this.tokenCache.get(agentId);
|
||||||
|
if (cached && now - cached.at < FabricInbound.TOKEN_TTL_MS) {
|
||||||
|
return pick(cached.session) ?? pick(fallback);
|
||||||
|
}
|
||||||
|
const apiKey = this.identity.findByAgentId(agentId)?.fabricApiKey;
|
||||||
|
if (apiKey) {
|
||||||
|
try {
|
||||||
|
const s = await this.client.agentLogin(apiKey);
|
||||||
|
this.tokenCache.set(agentId, { session: s, at: now });
|
||||||
|
return pick(s) ?? pick(fallback);
|
||||||
|
} catch (err) {
|
||||||
|
this.log.warn(`fabric: token refresh failed agent=${agentId}: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pick(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly core: unknown, // PluginRuntime
|
private readonly core: unknown, // PluginRuntime
|
||||||
@@ -188,7 +222,7 @@ export class FabricInbound {
|
|||||||
agentId: route.agentId,
|
agentId: route.agentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const gt = session.guildAccessTokens.find((t) => t.guildNodeId === guild.nodeId)?.token;
|
const gt = await this.freshGuildToken(agentId, guild.nodeId, session);
|
||||||
|
|
||||||
// Fetch any uploaded files for the agent: download to a temp dir and
|
// Fetch any uploaded files for the agent: download to a temp dir and
|
||||||
// hand openclaw local MediaPaths (+types) so the model receives them.
|
// hand openclaw local MediaPaths (+types) so the model receives them.
|
||||||
|
|||||||
Reference in New Issue
Block a user