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,