feat(security): commandsSyncKey is a required channel-config field (Guild C-2)
The slash-command sync secret now comes from channels.fabric.commandsSyncKey (configSchema marks it required) and is no longer read from FABRIC_COMMANDS_SYNC_KEY env. command-sync resolves it from config and threads it into client.syncCommands; when absent, sync is skipped with a clear warning. README updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
dist/fabric/src/accounts.js
vendored
6
dist/fabric/src/accounts.js
vendored
@@ -7,6 +7,12 @@ const DEFAULT_CENTER = 'http://localhost:7001/api';
|
||||
function section(cfg) {
|
||||
return cfg.channels?.fabric ?? {};
|
||||
}
|
||||
// The commands-sync shared secret (channel-level only). Empty string when
|
||||
// unconfigured — callers decide how to handle (slash-command sync is then
|
||||
// rejected by the guild).
|
||||
export function resolveCommandsSyncKey(cfg) {
|
||||
return (section(cfg).commandsSyncKey ?? '').trim();
|
||||
}
|
||||
export function listFabricAccountIds(cfg) {
|
||||
const accts = section(cfg).accounts ?? {};
|
||||
const ids = Object.keys(accts);
|
||||
|
||||
11
dist/fabric/src/command-sync.js
vendored
11
dist/fabric/src/command-sync.js
vendored
@@ -6,6 +6,7 @@
|
||||
// dynamic arg `choices` to a static snapshot here (like Discord does at
|
||||
// registration time).
|
||||
import { listNativeCommandSpecsForConfig, findCommandByNativeName, resolveCommandArgChoices, } from 'openclaw/plugin-sdk/native-command-registry';
|
||||
import { resolveCommandsSyncKey } from './accounts.js';
|
||||
function normChoice(c) {
|
||||
if (typeof c === 'string')
|
||||
return { value: c, label: c };
|
||||
@@ -62,6 +63,14 @@ export function buildFabricCommandSpecs(cfg) {
|
||||
// Push the catalog to every guild the known agents belong to (idempotent;
|
||||
// the catalog is OpenClaw-global, so one PUT per guild is enough).
|
||||
export async function syncFabricCommands(client, cfg, accounts, log) {
|
||||
// Guild C-2: the sync key comes from the channel config only (schema
|
||||
// marks it required). Without it the guild rejects the catalog write.
|
||||
const syncKey = resolveCommandsSyncKey(cfg);
|
||||
if (!syncKey) {
|
||||
log.warn('fabric: channels.fabric.commandsSyncKey is not set — skipping ' +
|
||||
'slash-command sync (set it to the guild FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY)');
|
||||
return;
|
||||
}
|
||||
let specs;
|
||||
try {
|
||||
specs = buildFabricCommandSpecs(cfg);
|
||||
@@ -88,7 +97,7 @@ export async function syncFabricCommands(client, cfg, accounts, log) {
|
||||
if (!tok)
|
||||
continue;
|
||||
try {
|
||||
await client.syncCommands(g.endpoint, tok, specs);
|
||||
await client.syncCommands(g.endpoint, tok, specs, syncKey);
|
||||
done.add(g.nodeId);
|
||||
log.info(`fabric: synced ${specs.length} slash command(s) -> ${g.nodeId}`);
|
||||
}
|
||||
|
||||
11
dist/fabric/src/fabric-client.js
vendored
11
dist/fabric/src/fabric-client.js
vendored
@@ -75,12 +75,11 @@ export class FabricClient {
|
||||
// Register the OpenClaw slash-command catalog with this guild (idempotent
|
||||
// full replace). The frontend GETs it for `/` autocomplete; execution
|
||||
// still flows as a normal /<cmd> message into OpenClaw's command system.
|
||||
syncCommands(guildEndpoint, guildToken, commands) {
|
||||
// Guild C-2: when the operator sets a shared sync key on both sides
|
||||
// (FABRIC_COMMANDS_SYNC_KEY here / FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY
|
||||
// on the guild), the catalog write is restricted to this plugin.
|
||||
const key = process.env.FABRIC_COMMANDS_SYNC_KEY;
|
||||
return this.req('PUT', `${guildEndpoint}/api/commands`, guildToken, { commands }, key ? { 'x-commands-sync-key': key } : undefined);
|
||||
syncCommands(guildEndpoint, guildToken, commands, syncKey) {
|
||||
// Guild C-2: the shared key is sourced from the channel config
|
||||
// (channels.fabric.commandsSyncKey) and must equal the guild's
|
||||
// FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY for the catalog write.
|
||||
return this.req('PUT', `${guildEndpoint}/api/commands`, guildToken, { commands }, syncKey ? { 'x-commands-sync-key': syncKey } : undefined);
|
||||
}
|
||||
// [{ userId, bypass }] — bypass is true only for discuss/work bypass-list
|
||||
channelMembers(guildEndpoint, guildToken, channelId) {
|
||||
|
||||
Reference in New Issue
Block a user