3 Commits

Author SHA1 Message Date
07e91ea858 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>
2026-06-05 15:39:14 +01:00
h z
06ccd3564e Merge pull request 'refactor(install): clone HarborForge.Cli to /tmp instead of fixed path' (#12) from refactor/install-cli-clone-from-repo into main 2026-05-29 07:52:40 +00:00
2977ab369e refactor(install): clone HarborForge.Cli to /tmp instead of fixed path
`installCli()` used to look for the CLI source at a fixed path relative
to the plugin checkout: either `./HarborForge.Cli` or `../HarborForge.Cli`.
That breaks any install layout where the plugin lives on its own — the
script just logs "Skipping CLI installation" and returns. Same anti-
pattern installManagedMonitor had already fixed for the monitor binary.

Mirror the monitor flow:

  1. git clone --depth 1 --branch <cliBranch> CLI_REPO_URL → /tmp/<dir>
  2. go build -ldflags Version=<date>+<branch>-<sha> -o $openclaw/bin/hf
  3. chmod 755 + delete tmp dir on success or failure

Adds `--cli-branch <name>` (default: main) for parity with --monitor-branch.

Also stamps the binary with a real version string (was 'dev' before this
patch) so `hf version` is informative for debugging.
2026-05-29 08:52:14 +01:00
2 changed files with 84 additions and 28 deletions

View File

@@ -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<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);
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,

View File

@@ -31,6 +31,7 @@ const OLD_PLUGIN_NAME = 'harborforge-monitor';
const PLUGIN_SRC_DIR = join(__dirname, 'plugin');
const SKILLS_SRC_DIR = join(__dirname, 'skills');
const MONITOR_REPO_URL = 'https://git.hangman-lab.top/zhi/HarborForge.Monitor.git';
const CLI_REPO_URL = 'https://git.hangman-lab.top/zhi/HarborForge.Cli.git';
const args = process.argv.slice(2);
const options = {
@@ -43,6 +44,7 @@ const options = {
installCli: args.includes('--install-cli'),
installMonitor: 'no',
monitorBranch: 'main',
cliBranch: 'main',
};
const profileIdx = args.indexOf('--openclaw-profile-path');
@@ -60,6 +62,11 @@ if (monitorBranchIdx !== -1 && args[monitorBranchIdx + 1]) {
options.monitorBranch = String(args[monitorBranchIdx + 1]);
}
const cliBranchIdx = args.indexOf('--cli-branch');
if (cliBranchIdx !== -1 && args[cliBranchIdx + 1]) {
options.cliBranch = String(args[cliBranchIdx + 1]);
}
function resolveOpenclawPath() {
if (options.openclawProfilePath) return options.openclawProfilePath;
if (process.env.OPENCLAW_PATH) return resolve(process.env.OPENCLAW_PATH);
@@ -321,34 +328,35 @@ async function installCli() {
const binDir = join(openclawPath, 'bin');
mkdirSync(binDir, { recursive: true });
// Find CLI source — look for HarborForge.Cli relative to project root
const projectRoot = resolve(__dirname, '..');
const cliDir = join(projectRoot, 'HarborForge.Cli');
if (!existsSync(cliDir)) {
// Try parent directory (monorepo layout)
const monoCliDir = resolve(projectRoot, '..', 'HarborForge.Cli');
if (!existsSync(monoCliDir)) {
logErr(`Cannot find HarborForge.Cli at ${cliDir} or ${monoCliDir}`);
logWarn('Skipping CLI installation');
return;
}
}
const effectiveCliDir = existsSync(cliDir)
? cliDir
: resolve(projectRoot, '..', 'HarborForge.Cli');
log(` Building hf from ${effectiveCliDir}...`, 'blue');
// Clone CLI repo to /tmp, build there, copy artifact out. Mirrors
// installManagedMonitor so the install never depends on a checked-out
// sibling repo at a fixed path.
const tmpDir = join('/tmp', `harborforge-cli-${Date.now()}`);
const hfBinary = join(binDir, 'hf');
try {
const hfBinary = join(binDir, 'hf');
exec(`go build -o ${hfBinary} ./cmd/hf`, { cwd: effectiveCliDir, silent: !options.verbose });
log(` Cloning ${CLI_REPO_URL} (branch ${options.cliBranch}) → ${tmpDir}...`, 'blue');
exec(`git clone --branch ${shellEscape(options.cliBranch)} --depth 1 ${shellEscape(CLI_REPO_URL)} ${shellEscape(tmpDir)}`, { silent: !options.verbose });
// Stamp the binary with the version string the prod CLI surfaces in
// `hf version`. Fall back to a date-only label if rev-parse fails for
// any reason (shallow clone shouldn't, but be defensive).
let versionLabel = `${new Date().toISOString().slice(0, 10)}+install`;
try {
const sha = exec(`git rev-parse --short HEAD`, { cwd: tmpDir, silent: true }).trim();
if (sha) versionLabel = `${new Date().toISOString().slice(0, 10)}+${options.cliBranch}-${sha}`;
} catch { /* keep fallback */ }
log(` Building hf (version=${versionLabel})...`, 'blue');
const ldflags = `-X git.hangman-lab.top/zhi/HarborForge.Cli/internal/commands.Version=${versionLabel}`;
exec(`go build -ldflags ${shellEscape(ldflags)} -o ${shellEscape(hfBinary)} ./cmd/hf`, { cwd: tmpDir, silent: !options.verbose });
chmodSync(hfBinary, 0o755);
logOk(`hf binary → ${hfBinary}`);
logOk(`hf binary → ${hfBinary} (branch hint: ${options.cliBranch})`);
} catch (err) {
logErr(`Failed to build hf CLI: ${err.message}`);
logWarn('CLI installation failed, plugin still installed');
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
}