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:
@@ -14,6 +14,10 @@ export type FabricAccountConfig = {
|
||||
|
||||
export type FabricChannelConfig = {
|
||||
centerApiBase?: string;
|
||||
// Shared secret matching the guild's FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY
|
||||
// (Guild C-2). Required by the channel config schema; sourced from config
|
||||
// only — never from the environment.
|
||||
commandsSyncKey?: string;
|
||||
accounts?: Record<string, FabricAccountConfig>;
|
||||
defaultAccount?: string;
|
||||
} & FabricAccountConfig;
|
||||
@@ -35,6 +39,13 @@ function section(cfg: Cfg): FabricChannelConfig {
|
||||
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: Cfg): string {
|
||||
return (section(cfg).commandsSyncKey ?? '').trim();
|
||||
}
|
||||
|
||||
export function listFabricAccountIds(cfg: Cfg): string[] {
|
||||
const accts = section(cfg).accounts ?? {};
|
||||
const ids = Object.keys(accts);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
resolveCommandArgChoices,
|
||||
} from 'openclaw/plugin-sdk/native-command-registry';
|
||||
import type { FabricClient } from './fabric-client.js';
|
||||
import { resolveCommandsSyncKey } from './accounts.js';
|
||||
|
||||
type Logger = { info: (m: string) => void; warn: (m: string) => void };
|
||||
|
||||
@@ -102,6 +103,17 @@ export async function syncFabricCommands(
|
||||
accounts: Array<{ agentId: string; fabricApiKey: string }>,
|
||||
log: Logger,
|
||||
): Promise<void> {
|
||||
// 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 as never);
|
||||
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: FabricCommand[];
|
||||
try {
|
||||
specs = buildFabricCommandSpecs(cfg);
|
||||
@@ -126,7 +138,7 @@ export async function syncFabricCommands(
|
||||
)?.token;
|
||||
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}`);
|
||||
} catch (err) {
|
||||
|
||||
@@ -124,17 +124,17 @@ export class FabricClient {
|
||||
guildEndpoint: string,
|
||||
guildToken: string,
|
||||
commands: unknown[],
|
||||
syncKey: string,
|
||||
): Promise<unknown> {
|
||||
// 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;
|
||||
// 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 },
|
||||
key ? { 'x-commands-sync-key': key } : undefined,
|
||||
syncKey ? { 'x-commands-sync-key': syncKey } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user