feat(plugin): gate registerTool via __padded.allowTool
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) <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,33 @@ interface PluginAPI {
|
|||||||
* wrap every tool's execute return; doing it at the `registerTool` boundary
|
* wrap every tool's execute return; doing it at the `registerTool` boundary
|
||||||
* keeps each tool body unchanged.
|
* 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 }> } {
|
function ensureMcpContentShape(result: unknown): { content: Array<{ type: 'text'; text: string }> } {
|
||||||
if (
|
if (
|
||||||
result && typeof result === 'object' &&
|
result && typeof result === 'object' &&
|
||||||
@@ -105,14 +132,35 @@ function register(api: PluginAPI): void {
|
|||||||
warn: (...args: any[]) => console.warn('[HarborForge]', ...args),
|
warn: (...args: any[]) => console.warn('[HarborForge]', ...args),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wrap api.registerTool so every tool's execute() return is coerced into
|
// PaddedCell tools-cache integration (decision #37, openclaw side).
|
||||||
// the MCP `{ content: [...] }` shape openclaw expects. See
|
// Stub the global API early so the gate is consistent regardless of
|
||||||
// `ensureMcpContentShape` above.
|
// plugin load order; PaddedCell will replace stub with the real impl
|
||||||
|
// when it loads. fail-open until then.
|
||||||
|
ensurePaddedStub();
|
||||||
|
const seenForCatalog = new Set<string>();
|
||||||
|
|
||||||
|
// 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);
|
const _origRegisterTool = api.registerTool.bind(api);
|
||||||
api.registerTool = (factory: (ctx: any) => any) => {
|
api.registerTool = (factory: (ctx: any) => any) => {
|
||||||
_origRegisterTool((ctx: any) => {
|
_origRegisterTool((ctx: any) => {
|
||||||
const def = factory(ctx);
|
const def = factory(ctx);
|
||||||
if (!def || typeof def.execute !== 'function') return def;
|
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;
|
const origExecute = def.execute;
|
||||||
return {
|
return {
|
||||||
...def,
|
...def,
|
||||||
|
|||||||
Reference in New Issue
Block a user