From 07e91ea85899bbd1fd33e0add562d38a7f689d18 Mon Sep 17 00:00:00 2001 From: hzhang Date: Fri, 5 Jun 2026 15:39:14 +0100 Subject: [PATCH] feat(plugin): gate registerTool via __padded.allowTool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires HF into the PaddedCell tools-cache filter (decision #37, openclaw side). Hooks into the existing api.registerTool wrap (originally added for ensureMcpContentShape) so each tool: 1. Registers name+description into PaddedCell's catalog 2. Returns null when the per-session cache doesn't include the name (the model doesn't see it that turn) 3. Has its execute() return coerced to MCP content shape (preserved from earlier) Fail-open stub installed if PaddedCell hasn't loaded yet. All 9 HF tools (status, telemetry, monitor_telemetry, calendar_*, restart_status) are gate-able by default — agents `dynamic-cache-tools` them before use. Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin/index.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/plugin/index.ts b/plugin/index.ts index a636ffe..26b3969 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -86,6 +86,33 @@ interface PluginAPI { * wrap every tool's execute return; doing it at the `registerTool` boundary * keeps each tool body unchanged. */ +/** + * Install a fail-open globalThis.__padded stub if PaddedCell hasn't loaded + * yet (load order isn't guaranteed). PaddedCell's installGlobalApi drains + * `_pendingCatalog` and replaces `allowTool` with the real check when it + * starts. This means HF tools registered before PaddedCell are visible + * to the agent until PaddedCell takes over, after which they fall under + * the per-session cache gate (decision #37, openclaw side). + */ +function ensurePaddedStub(): void { + const g = globalThis as unknown as { + __padded?: { + _pendingCatalog?: Array<{ name: string; description: string }>; + registerCatalogEntry?: (n: string, d: string) => void; + allowTool?: (n: string, c: unknown) => boolean; + }; + }; + 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, + }; +} + function ensureMcpContentShape(result: unknown): { content: Array<{ type: 'text'; text: string }> } { if ( result && typeof result === 'object' && @@ -105,14 +132,35 @@ function register(api: PluginAPI): void { warn: (...args: any[]) => console.warn('[HarborForge]', ...args), }; - // Wrap api.registerTool so every tool's execute() return is coerced into - // the MCP `{ content: [...] }` shape openclaw expects. See - // `ensureMcpContentShape` above. + // PaddedCell tools-cache integration (decision #37, openclaw side). + // Stub the global API early so the gate is consistent regardless of + // plugin load order; PaddedCell will replace stub with the real impl + // when it loads. fail-open until then. + ensurePaddedStub(); + const seenForCatalog = new Set(); + + // Wrap api.registerTool so every tool: + // (a) registers its name+description into PaddedCell's catalog so + // dynamic-list-tools / dynamic-search-tools surface it (#37) + // (b) returns null when the per-session cache doesn't include the + // name → the tool is hidden from the model that turn + // (c) has its execute() return coerced into the MCP `{ content: [...] }` + // shape openclaw expects (preserved from earlier). const _origRegisterTool = api.registerTool.bind(api); api.registerTool = (factory: (ctx: any) => any) => { _origRegisterTool((ctx: any) => { const def = factory(ctx); if (!def || typeof def.execute !== 'function') return def; + const padded = (globalThis as any).__padded as + | { allowTool?: (n: string, c: any) => boolean; registerCatalogEntry?: (n: string, d: string) => void } + | undefined; + if (def.name && padded?.registerCatalogEntry && !seenForCatalog.has(def.name)) { + padded.registerCatalogEntry(def.name, def.description ?? ''); + seenForCatalog.add(def.name); + } + if (def.name && padded?.allowTool && !padded.allowTool(def.name, ctx)) { + return null; + } const origExecute = def.execute; return { ...def,