feat(tools): gate registerTool via __padded.allowTool
Wires Fabric into the PaddedCell tools-cache filter (decision #37, openclaw side). Each registerTool call is now wrapped to: 1. Register name+description into PaddedCell's catalog so dynamic-list-tools / dynamic-search-tools surface the tool 2. Return null when the per-session cache doesn't include the name (the model doesn't see it that turn) Fail-open stub installed if PaddedCell hasn't loaded yet — the stub queues catalog entries on `_pendingCatalog`, which PaddedCell drains when it initializes. All 13 Fabric tools (fabric-register, 4 channel creates, sub-discussion control, send/canvas, channel/guild list, purpose, message-history) gated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
65
src/tools.ts
65
src/tools.ts
@@ -35,6 +35,30 @@ const GREETING_DELAY_MS = (() => {
|
|||||||
return Number.isFinite(v) && v >= 0 ? v : 500;
|
return Number.isFinite(v) && v >= 0 ? v : 500;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// PaddedCell tools-cache integration (cross-runtime alignment with
|
||||||
|
// Plexum decision #37; openclaw lacks a before_outgoing_tools hook so we
|
||||||
|
// opt in per-plugin). All Fabric tools are gate-able by default — agents
|
||||||
|
// must `dynamic-cache-tools` them before the model sees them.
|
||||||
|
// `gatedRegister` wraps openclaw's api.registerTool: each factory invocation
|
||||||
|
// (1) registers the tool's name+description into PaddedCell's catalog
|
||||||
|
// so dynamic-list-tools / dynamic-search-tools surface it
|
||||||
|
// (2) returns null if the per-session cache doesn't include the name
|
||||||
|
// Buffer-drain pattern handles plugin load order — if PaddedCell hasn't
|
||||||
|
// loaded yet, the stub default-allows + queues catalog entries; PaddedCell
|
||||||
|
// drains the queue when it installs the real API.
|
||||||
|
function ensurePaddedStub(): void {
|
||||||
|
const g = globalThis as unknown as { __padded?: Record<string, unknown> & { _pendingCatalog?: Array<{ name: string; description: string }>; allowTool?: unknown; registerCatalogEntry?: unknown } };
|
||||||
|
if (g.__padded) return;
|
||||||
|
const buf: Array<{ name: string; description: string }> = [];
|
||||||
|
g.__padded = {
|
||||||
|
_pendingCatalog: buf,
|
||||||
|
registerCatalogEntry(name: string, description: string): void {
|
||||||
|
buf.push({ name, description });
|
||||||
|
},
|
||||||
|
allowTool: () => true, // fail-open until PaddedCell installs the real API
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function registerFabricTools(
|
export function registerFabricTools(
|
||||||
api: ToolApi,
|
api: ToolApi,
|
||||||
client: FabricClient,
|
client: FabricClient,
|
||||||
@@ -42,6 +66,21 @@ export function registerFabricTools(
|
|||||||
store: SubDiscussionStore,
|
store: SubDiscussionStore,
|
||||||
cfg: ToolsCfg,
|
cfg: ToolsCfg,
|
||||||
): void {
|
): void {
|
||||||
|
ensurePaddedStub();
|
||||||
|
const seenForCatalog = new Set<string>();
|
||||||
|
const gatedRegister = (factory: (ctx: Ctx) => unknown | null): void => {
|
||||||
|
api.registerTool((ctx: Ctx) => {
|
||||||
|
const tool = factory(ctx) as { name?: string; description?: string } | null;
|
||||||
|
if (!tool || !tool.name) return tool;
|
||||||
|
const padded = (globalThis as unknown as { __padded?: { allowTool?: (n: string, c: Ctx) => boolean; registerCatalogEntry?: (n: string, d: string) => void } }).__padded;
|
||||||
|
if (padded?.registerCatalogEntry && !seenForCatalog.has(tool.name)) {
|
||||||
|
padded.registerCatalogEntry(tool.name, tool.description ?? '');
|
||||||
|
seenForCatalog.add(tool.name);
|
||||||
|
}
|
||||||
|
if (padded?.allowTool && !padded.allowTool(tool.name, ctx)) return null;
|
||||||
|
return tool;
|
||||||
|
});
|
||||||
|
};
|
||||||
// Resolve the calling agent's Fabric session + a guild's token/endpoint.
|
// Resolve the calling agent's Fabric session + a guild's token/endpoint.
|
||||||
const ctxGuild = async (agentId: string, guildNodeId: string) => {
|
const ctxGuild = async (agentId: string, guildNodeId: string) => {
|
||||||
const entry = identity.findByAgentId(agentId);
|
const entry = identity.findByAgentId(agentId);
|
||||||
@@ -65,7 +104,7 @@ export function registerFabricTools(
|
|||||||
// still exists for one-time bootstrap before the gateway runs, but
|
// still exists for one-time bootstrap before the gateway runs, but
|
||||||
// recruitment's `register-agent` script should prefer this tool path
|
// recruitment's `register-agent` script should prefer this tool path
|
||||||
// so the new agent's socket is live before `interviewer` fires.
|
// so the new agent's socket is live before `interviewer` fires.
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'fabric-register',
|
name: 'fabric-register',
|
||||||
description:
|
description:
|
||||||
'Bind an agent to a Fabric Center API key. Validates the key, writes ' +
|
'Bind an agent to a Fabric Center API key. Validates the key, writes ' +
|
||||||
@@ -128,7 +167,7 @@ export function registerFabricTools(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const makeCreate = (kind: 'chat' | 'work' | 'report' | 'discussion') =>
|
const makeCreate = (kind: 'chat' | 'work' | 'report' | 'discussion') =>
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: `create-${kind}-channel`,
|
name: `create-${kind}-channel`,
|
||||||
description:
|
description:
|
||||||
`Create a Fabric ${kind} channel (x_type=${X_BY_KIND[kind]}). ` +
|
`Create a Fabric ${kind} channel (x_type=${X_BY_KIND[kind]}). ` +
|
||||||
@@ -184,7 +223,7 @@ export function registerFabricTools(
|
|||||||
|
|
||||||
// discussion-complete: post a summary then close the channel (Guild
|
// discussion-complete: post a summary then close the channel (Guild
|
||||||
// /channels/:id/close — history stays readable, new posts -> 409).
|
// /channels/:id/close — history stays readable, new posts -> 409).
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'discussion-complete',
|
name: 'discussion-complete',
|
||||||
description: 'Conclude a discussion: post a summary then close the channel.',
|
description: 'Conclude a discussion: post a summary then close the channel.',
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -242,7 +281,7 @@ export function registerFabricTools(
|
|||||||
// session prompt whenever a turn in this channel fires — so the
|
// session prompt whenever a turn in this channel fires — so the
|
||||||
// two roles see different instructions, no shared guide file.
|
// two roles see different instructions, no shared guide file.
|
||||||
// ───────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'create-sub-discussion',
|
name: 'create-sub-discussion',
|
||||||
description:
|
description:
|
||||||
'Open a host-driven sub-discussion channel (x_type=discuss) hanging off your current channel, ' +
|
'Open a host-driven sub-discussion channel (x_type=discuss) hanging off your current channel, ' +
|
||||||
@@ -363,7 +402,7 @@ export function registerFabricTools(
|
|||||||
// author, not the host's personal account — and can wake the host on
|
// author, not the host's personal account — and can wake the host on
|
||||||
// the parent channel to continue whatever workflow opened the sub.
|
// the parent channel to continue whatever workflow opened the sub.
|
||||||
// ───────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'close-sub-discussion',
|
name: 'close-sub-discussion',
|
||||||
description:
|
description:
|
||||||
'Close a sub-discussion channel you opened (host-only) and write a callback to the parent ' +
|
'Close a sub-discussion channel you opened (host-only) and write a callback to the parent ' +
|
||||||
@@ -460,7 +499,7 @@ export function registerFabricTools(
|
|||||||
// fabric-canvas: share / update / read / close the channel's single
|
// fabric-canvas: share / update / read / close the channel's single
|
||||||
// pinned canvas document (one tool, four actions). update/close are
|
// pinned canvas document (one tool, four actions). update/close are
|
||||||
// sharer-only server-side (the guild returns 403 otherwise).
|
// sharer-only server-side (the guild returns 403 otherwise).
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'fabric-canvas',
|
name: 'fabric-canvas',
|
||||||
description:
|
description:
|
||||||
"Manage a channel's pinned canvas document. action: " +
|
"Manage a channel's pinned canvas document. action: " +
|
||||||
@@ -537,7 +576,7 @@ export function registerFabricTools(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// fabric-channel: channel membership (one tool, three actions).
|
// fabric-channel: channel membership (one tool, three actions).
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'fabric-channel',
|
name: 'fabric-channel',
|
||||||
description:
|
description:
|
||||||
'Channel membership. action: members (list channel member userIds) | ' +
|
'Channel membership. action: members (list channel member userIds) | ' +
|
||||||
@@ -589,7 +628,7 @@ export function registerFabricTools(
|
|||||||
// workload to #agents-room, or triage agent following up on an
|
// workload to #agents-room, or triage agent following up on an
|
||||||
// already-routed task by commenting in #updates.
|
// already-routed task by commenting in #updates.
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'fabric-send-message',
|
name: 'fabric-send-message',
|
||||||
description:
|
description:
|
||||||
'Send a text message into a specific Fabric channel. Author is the calling agent. ' +
|
'Send a text message into a specific Fabric channel. Author is the calling agent. ' +
|
||||||
@@ -634,7 +673,7 @@ export function registerFabricTools(
|
|||||||
// config → tool returns ok:false with a clear error, no fall-through
|
// config → tool returns ok:false with a clear error, no fall-through
|
||||||
// to regular agent posting.
|
// to regular agent posting.
|
||||||
// ───────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'fabric-send-sys-msg',
|
name: 'fabric-send-sys-msg',
|
||||||
description:
|
description:
|
||||||
'Send a SYSTEM-AUTHORED message into a Fabric channel (author = guild sentinel, not you). ' +
|
'Send a SYSTEM-AUTHORED message into a Fabric channel (author = guild sentinel, not you). ' +
|
||||||
@@ -708,7 +747,7 @@ export function registerFabricTools(
|
|||||||
// agent is a member of. Returns id / name / xType per channel so the
|
// agent is a member of. Returns id / name / xType per channel so the
|
||||||
// agent can pick a channelId for fabric-send-message etc.
|
// agent can pick a channelId for fabric-send-message etc.
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'fabric-channel-list',
|
name: 'fabric-channel-list',
|
||||||
description:
|
description:
|
||||||
'List channels visible to the calling agent in a guild. Optional ' +
|
'List channels visible to the calling agent in a guild. Optional ' +
|
||||||
@@ -769,7 +808,7 @@ export function registerFabricTools(
|
|||||||
// workflow says "find the right guild for X" — pick by purpose,
|
// workflow says "find the right guild for X" — pick by purpose,
|
||||||
// then fabric-channel-list to find the right channel inside it.
|
// then fabric-channel-list to find the right channel inside it.
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'fabric-guild-list',
|
name: 'fabric-guild-list',
|
||||||
description:
|
description:
|
||||||
'List guilds the calling agent is a member of. Returns ' +
|
'List guilds the calling agent is a member of. Returns ' +
|
||||||
@@ -830,7 +869,7 @@ export function registerFabricTools(
|
|||||||
// channel must be public). Use this to backfill purpose on existing
|
// channel must be public). Use this to backfill purpose on existing
|
||||||
// channels, or to refine it after a channel's role evolves.
|
// channels, or to refine it after a channel's role evolves.
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'fabric-channel-set-purpose',
|
name: 'fabric-channel-set-purpose',
|
||||||
description:
|
description:
|
||||||
"Set or update a channel's free-form purpose description. " +
|
"Set or update a channel's free-form purpose description. " +
|
||||||
@@ -873,7 +912,7 @@ export function registerFabricTools(
|
|||||||
// gated; verify a previous message went through; lookup recent
|
// gated; verify a previous message went through; lookup recent
|
||||||
// duplicates before opening a new task in triage.
|
// duplicates before opening a new task in triage.
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
api.registerTool((ctx: Ctx) => ({
|
gatedRegister((ctx: Ctx) => ({
|
||||||
name: 'fabric-message-history',
|
name: 'fabric-message-history',
|
||||||
description:
|
description:
|
||||||
"Read a channel's recent message history. Omit seqFrom/seqTo to " +
|
"Read a channel's recent message history. Omit seqFrom/seqTo to " +
|
||||||
|
|||||||
Reference in New Issue
Block a user