This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -1,5 +1,10 @@
# @waggle/hive-mind-hooks-openclaw
> **Roadmap-only in the current Waggle Windows Solo release.** This package is
> retained for development and future qualification. The production manifest
> does not expose OpenClaw launch, hook installation, Fleet/task dispatch, or
> direct agent runs. The implementation notes below do not override that gate.
Silent-capture shim that wires **OpenClaw** (`openclaw/openclaw`) gateway
lifecycle hooks into [hive-mind](https://github.com/marolinik/hive-mind)
frames. Every OpenClaw conversation deterministically captures

View File

@@ -58,10 +58,9 @@ function printHelp(): void {
' --help, -h Show this help.',
' --handler-source <PATH> Override compiled handler.js source (testing).',
' --cli-path <PATH> Absolute path to the hive-mind-cli binary or its',
' compiled JS entry. Required on Windows (npm bin',
' is a .cmd shim) and recommended for production',
' installs. Threaded into the hook entry env as',
' WAGGLE_HIVE_MIND_CLI.',
' compiled JS entry. Required for managed install',
' and verify. Verify treats it as the authoritative',
' packaged CLI expectation.',
'',
'Repo: https://github.com/marolinik/waggle-os',
'',
@@ -159,7 +158,11 @@ async function main(): Promise<void> {
return;
}
if (command === 'verify') {
const result = await verify(baseOpts);
const result = await verify({
...baseOpts,
...(cliPath !== undefined ? { cliPath } : {}),
requireManagedRuntime: true,
});
printVerifySummary(result);
if (!result.ok) process.exit(1);
return;

View File

@@ -33,13 +33,15 @@
* this package's tree (or have the deps bundled). Documented in the README.
*/
import { spawn } from 'node:child_process';
import {
createCliBridge,
createLogger,
type CliBridgeOptions,
type MemoryHit,
type SpawnFn,
} from '@waggle/hive-mind-shim-core';
import {
buildHookBridgeOptions,
makeOpenclawHandler,
type HookContext,
type InternalHookEventLike,
@@ -134,19 +136,64 @@ function extractFor(
}
}
/** Build a CliBridge, honoring an install-pinned cli path from env. */
function buildBridge(): ReturnType<typeof createCliBridge> {
export interface OpenclawHookRuntimeOptions {
/** Install-pinned CLI path embedded into the managed handler loader. */
readonly cliPath?: string;
/** Install-pinned Node executable used for JavaScript CLI entries. */
readonly nodePath?: string;
}
/** Build a CliBridge, preferring the loader-pinned path over ambient env. */
function buildBridge(opts: OpenclawHookRuntimeOptions = {}): ReturnType<typeof createCliBridge> {
const logger = createLogger({ name: 'openclaw-hooks/handler' });
const cliPath = process.env.WAGGLE_HIVE_MIND_CLI;
const opts: CliBridgeOptions = { logger };
if (typeof cliPath === 'string' && cliPath.length > 0) opts.cli_path = cliPath;
return createCliBridge(opts);
const envCliPath = process.env.WAGGLE_HIVE_MIND_CLI;
const cliPath = typeof opts.cliPath === 'string' && opts.cliPath.length > 0
? opts.cliPath
: envCliPath;
const nodePath = typeof opts.nodePath === 'string' && opts.nodePath.length > 0
? opts.nodePath
: undefined;
const isJavaScriptCli = typeof cliPath === 'string'
&& /\.(?:c|m)?js$/i.test(cliPath);
const pinnedNodeSpawn: SpawnFn | undefined = isJavaScriptCli && nodePath
? (_command, args, spawnOptions) => (
spawnOptions === undefined
? spawn(nodePath, args)
: spawn(nodePath, args, spawnOptions)
)
: undefined;
// OpenClaw shares its gateway event loop with hooks, so every bridge call —
// including pre-compact cleanup — is intentionally best-effort and bounded.
// A stalled CLI must release the gateway instead of delaying compaction.
return createCliBridge({
...buildHookBridgeOptions(
logger,
typeof cliPath === 'string' && cliPath.length > 0 ? cliPath : undefined,
),
...(pinnedNodeSpawn ? { spawnImpl: pinnedNodeSpawn } : {}),
});
}
const handler = makeOpenclawHandler(openclawAdapter, {
stopDebounceMs: DEFAULT_STOP_DEBOUNCE_MS,
});
const pendingPromptSnapshots = new Map<string, Promise<MemoryHit[]>>();
const PENDING_PROMPT_SNAPSHOT_TTL_MS = 5 * 60_000;
function promptSnapshotKey(event: OpenclawRuntimeEvent, fallback: string): string {
const key = event.sessionKey;
return typeof key === 'string' && key.length > 0 ? key : fallback;
}
function rememberPromptSnapshot(key: string, snapshot: Promise<MemoryHit[]>): void {
pendingPromptSnapshots.set(key, snapshot);
const expiry = setTimeout(() => {
if (pendingPromptSnapshots.get(key) === snapshot) pendingPromptSnapshots.delete(key);
}, PENDING_PROMPT_SNAPSHOT_TTL_MS);
expiry.unref?.();
}
/**
* The OpenClaw default export. Receives the runtime `InternalHookEvent`, maps
* `event.context` → the extracted payload, and drives the shared bodies.
@@ -158,23 +205,68 @@ const handler = makeOpenclawHandler(openclawAdapter, {
*
* NEVER throws — always returns a resolved promise (fail-open).
*/
export default async function openclawHook(event: OpenclawRuntimeEvent): Promise<void> {
export default async function openclawHook(
event: OpenclawRuntimeEvent,
runtimeOptions: OpenclawHookRuntimeOptions = {},
): Promise<void> {
try {
const lifecycle = lifecycleFor(event);
if (lifecycle === undefined) return;
const ctx = asContext(event);
const extracted = extractFor(lifecycle, ctx);
if (lifecycle === 'user-prompt-submit') {
const prompt = extracted as UserPromptExtracted;
const key = promptSnapshotKey(event, prompt.sessionId);
const previous = pendingPromptSnapshots.get(key);
const snapshot = (previous ?? Promise.resolve<MemoryHit[]>([]))
.catch(() => [])
.then(async () => {
const bridge = buildBridge(runtimeOptions);
let hits: MemoryHit[] = [];
try {
hits = await bridge.recallMemory('', {
limit: DEFAULT_RECALL_LIMIT,
scope: 'personal',
});
} catch {
// Fail open: prompt capture still runs if historical recall fails.
}
const hookCtx: HookContext = {
bridge,
logger: createLogger({ name: 'openclaw-hooks/handler' }),
};
await handler.handle({ event, extracted: prompt }, hookCtx);
return hits;
})
.catch(() => []);
rememberPromptSnapshot(key, snapshot);
await snapshot;
return;
}
// SessionStart: drive recall ourselves so we can mutate bootstrapFiles
// with the injected text (the shared body's stdout return is unused
// in-process).
if (lifecycle === 'session-start') {
await injectBootstrap(ctx, extracted as SessionStartExtracted);
const sessionStart = extracted as SessionStartExtracted;
const key = promptSnapshotKey(event, sessionStart.sessionId ?? provenanceScope(ctx));
const snapshot = pendingPromptSnapshots.get(key);
if (snapshot) {
const hits = await snapshot;
if (pendingPromptSnapshots.get(key) === snapshot) pendingPromptSnapshots.delete(key);
await injectBootstrap(ctx, sessionStart, runtimeOptions, hits);
} else {
await injectBootstrap(ctx, sessionStart, runtimeOptions);
}
return;
}
const input: OpenclawHandlerInput = { event, extracted };
const hookCtx: HookContext = { bridge: buildBridge(), logger: createLogger({ name: 'openclaw-hooks/handler' }) };
const hookCtx: HookContext = {
bridge: buildBridge(runtimeOptions),
logger: createLogger({ name: 'openclaw-hooks/handler' }),
};
await handler.handle(input, hookCtx);
} catch {
// FAIL-OPEN: swallow — the gateway flow must never be affected.
@@ -190,11 +282,13 @@ export default async function openclawHook(event: OpenclawRuntimeEvent): Promise
async function injectBootstrap(
ctx: OpenclawRuntimeContext,
extracted: SessionStartExtracted,
runtimeOptions: OpenclawHookRuntimeOptions = {},
snapshot?: readonly MemoryHit[],
): Promise<void> {
const logger = createLogger({ name: 'openclaw-hooks/handler' });
try {
const bridge = buildBridge();
const hits: MemoryHit[] = await bridge.recallMemory('', {
const hits: readonly MemoryHit[] = snapshot
?? await buildBridge(runtimeOptions).recallMemory('', {
limit: extracted.recallLimit,
scope: 'personal',
});
@@ -203,7 +297,7 @@ async function injectBootstrap(
const arr = ctx.bootstrapFiles;
if (Array.isArray(arr)) {
// Mutate the host-owned array in place — this IS the injection seam.
(arr as unknown[]).push(text);
(arr as unknown[]).push(createRecallBootstrapFile(text));
} else {
// The host did not provide a mutable array; nothing to inject into.
logger.debug('agent:bootstrap had no bootstrapFiles array — skipping inject');
@@ -213,6 +307,21 @@ async function injectBootstrap(
}
}
export interface OpenclawBootstrapFile {
path: string;
name: string;
content: string;
}
/** Build the virtual context file shape accepted by OpenClaw's sanitizer. */
export function createRecallBootstrapFile(content: string): OpenclawBootstrapFile {
return {
path: 'HIVE_MIND_RECALL.md',
name: 'HIVE_MIND_RECALL.md',
content,
};
}
const PER_HIT_BUDGET = 240;
function formatHits(hits: readonly MemoryHit[]): string {

View File

@@ -14,9 +14,9 @@
* lossy — comments / trailing commas are dropped — so reversibility relies
* on the literal backup, not a re-serialized diff.)
* 3. Write the managed hook DIRECTORY
* `~/.openclaw/hooks/hive-mind/{HOOK.md, handler.js}` — HOOK.md declares
* our events, handler.js is the compiled in-process default export COPIED
* from this package's dist/.
* `~/.openclaw/hooks/hive-mind/{HOOK.md, handler.js, handler.cjs,
* package.json}`. The discoverable handler.js loads the self-contained
* CommonJS bundle from handler.cjs under a hook-local module boundary.
* 4. Minimal-touch edit of openclaw.json: flip `hooks.internal.enabled=true`
* + add `hooks.internal.entries["hive-mind"]={enabled:true, env?}` via
* `jsonRegister`. Existing config preserved verbatim.
@@ -29,7 +29,8 @@
import { readFile, writeFile, mkdir, copyFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { dirname } from 'node:path';
import { createHash } from 'node:crypto';
import { dirname, join } from 'node:path';
import { createLogger, type Logger } from '@waggle/hive-mind-shim-core';
import {
backupByteIdentical,
@@ -62,6 +63,8 @@ export interface InstallResult {
createdByUs: boolean;
/** The cli_path embedded in the entry env (undefined = default lookup at runtime). */
cliPath?: string;
/** The packaged Node runtime embedded into the managed loader. */
nodePath?: string;
}
export interface InstallOptions extends ResolvePathsOptions {
@@ -76,18 +79,61 @@ export interface InstallOptions extends ResolvePathsOptions {
* in-process handler's CliBridge uses it.
*/
cliPath?: string;
/** Packaged Node executable used for JavaScript CLI entries. */
nodePath?: string;
/** Extra env to attach to the hook entry (e.g. WAGGLE_WORKSPACE_ID). */
env?: Record<string, string>;
}
export interface OpenclawRuntimeBinding {
version: 1;
node_path: string;
node_sha256: string;
cli_path: string;
cli_sha256: string;
}
const POINTER_VERSION = '0.1.0';
const TOUCHED_KEYS = ['hooks.internal.enabled', `hooks.internal.entries.${HIVE_HOOK_ENTRY_KEY}`] as const;
const LIFECYCLE_NAMES = ['session-start', 'user-prompt-submit', 'stop', 'pre-compact'] as const;
export const OPENCLAW_HANDLER_BUNDLE = 'handler.cjs';
const OPENCLAW_HANDLER_ENTRY = 'handler.js';
export function renderOpenclawHandlerEntrySource(cliPath?: string, nodePath?: string): string {
if (cliPath === undefined) {
return `'use strict';\nmodule.exports = require('./${OPENCLAW_HANDLER_BUNDLE}');\n`;
}
const runtimeOptions = nodePath === undefined
? `{ cliPath: ${JSON.stringify(cliPath)} }`
: JSON.stringify({ cliPath, nodePath });
return [
`'use strict';`,
`const handler = require('./${OPENCLAW_HANDLER_BUNDLE}');`,
`module.exports = (event) => handler(event, ${runtimeOptions});`,
'',
].join('\n');
}
export const OPENCLAW_HANDLER_ENTRY_SOURCE = renderOpenclawHandlerEntrySource();
export const OPENCLAW_HANDLER_PACKAGE_JSON = `${JSON.stringify({ private: true, type: 'commonjs' }, null, 2)}\n`;
async function ensureDir(p: string): Promise<void> {
if (!existsSync(p)) await mkdir(p, { recursive: true });
}
function isJavaScriptPath(filePath: string): boolean {
return /\.(?:c|m)?js$/i.test(filePath);
}
async function sha256Required(filePath: string, label: string): Promise<string> {
try {
return createHash('sha256').update(await readFile(filePath)).digest('hex');
} catch (error) {
throw new Error(
`${label} is not readable at ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
export async function install(opts: InstallOptions = {}): Promise<InstallResult> {
const log = opts.logger ?? createLogger({ name: 'openclaw-hooks/install' });
const paths = resolvePaths({
@@ -114,24 +160,50 @@ export async function install(opts: InstallOptions = {}): Promise<InstallResult>
const { backupPath } = await backupByteIdentical(paths.configPath, now().toISOString());
if (backupPath) log.info('openclaw.json backed up', { backupPath });
// Write the managed hook dir: HOOK.md + the compiled handler.js (copied from
// this package's dist/). We COPY rather than reference so the gateway loads a
// stable file even if this package is removed (the handler still requires the
// @waggle/* deps at runtime — the OQ-5 live-install caveat, see README).
// Copy the self-contained bundle so the gateway remains independent of this
// package after installation.
await ensureDir(paths.hiveHookDir);
await writeFile(paths.hookMdPath, renderHookMd('handler.js'), 'utf-8');
const cliPath = normalizeCliPath(opts.cliPath);
const nodePathCandidate = normalizeCliPath(
opts.nodePath ?? process.env.WAGGLE_HOOK_NODE_PATH,
);
let runtimeBinding: OpenclawRuntimeBinding | undefined;
if (cliPath !== undefined && isJavaScriptPath(cliPath) && nodePathCandidate !== undefined) {
const [nodeSha256, cliSha256] = await Promise.all([
sha256Required(nodePathCandidate, 'packaged Node runtime'),
sha256Required(cliPath, 'pinned hive-mind CLI'),
]);
runtimeBinding = {
version: 1,
node_path: nodePathCandidate,
node_sha256: nodeSha256,
cli_path: cliPath,
cli_sha256: cliSha256,
};
}
const nodePath = runtimeBinding?.node_path;
await writeFile(paths.hookMdPath, renderHookMd(OPENCLAW_HANDLER_ENTRY), 'utf-8');
if (!existsSync(paths.handlerSourcePath)) {
throw new Error(
`compiled handler not found at ${paths.handlerSourcePath}. ` +
`Build the package (tsc --build) before installing.`,
);
}
await copyFile(paths.handlerSourcePath, paths.installedHandlerPath);
// OpenClaw discovers handler.js by filename. Keep that entry deterministic
// CommonJS while retaining the self-contained bundle's .cjs identity; the
// hook-local package.json overrides any ancestor `type: module` boundary.
await copyFile(paths.handlerSourcePath, join(paths.hiveHookDir, OPENCLAW_HANDLER_BUNDLE));
await writeFile(
paths.installedHandlerPath,
renderOpenclawHandlerEntrySource(cliPath, nodePath),
'utf-8',
);
await writeFile(join(paths.hiveHookDir, 'package.json'), OPENCLAW_HANDLER_PACKAGE_JSON, 'utf-8');
// Minimal-touch config edit.
const cliPath = normalizeCliPath(opts.cliPath);
const env: Record<string, string> = { ...(opts.env ?? {}) };
if (cliPath !== undefined) env['WAGGLE_HIVE_MIND_CLI'] = cliPath;
if (nodePath !== undefined) env['WAGGLE_HOOK_NODE_PATH'] = nodePath;
const merged = jsonRegister(existingConfig, Object.keys(env).length > 0 ? { env } : {});
await writeFile(paths.configPath, serializeConfig(merged), 'utf-8');
@@ -148,11 +220,17 @@ export async function install(opts: InstallOptions = {}): Promise<InstallResult>
extra: {
hook_dir_name: HIVE_HOOK_DIR_NAME,
touched_keys: TOUCHED_KEYS,
runtime_binding: runtimeBinding ?? null,
},
};
await writePointer(paths.pointerPath, pointer);
log.info('install complete', { createdByUs, hookDir: paths.hiveHookDir, cliPath: cliPath ?? '(PATH lookup)' });
log.info('install complete', {
createdByUs,
hookDir: paths.hiveHookDir,
cliPath: cliPath ?? '(PATH lookup)',
nodePath: nodePath ?? '(host runtime)',
});
const result: InstallResult = {
paths,
@@ -164,5 +242,6 @@ export async function install(opts: InstallOptions = {}): Promise<InstallResult>
createdByUs,
};
if (cliPath !== undefined) result.cliPath = cliPath;
if (nodePath !== undefined) result.nodePath = nodePath;
return result;
}

View File

@@ -5,18 +5,26 @@
* answers a `--help` probe. Plus the openclaw-specific activation advisory
* (spec §5.5 / §6.2): hooks are OFF until `hooks.internal.enabled=true`.
*
* Probe priority for `cli_path`:
* 1. Explicit `opts.cliPath` (caller override)
* 2. `cli_path` recorded in `~/.openclaw/hive-mind-install.json`
* 3. Bare `'hive-mind-cli'` on PATH
* Probe selection follows the managed runtime: the trusted install pin, or
* bare `'hive-mind-cli'` on PATH when the loader is unpinned.
*/
import { readFile, access } from 'node:fs/promises';
import { constants, existsSync } from 'node:fs';
import { spawn } from 'node:child_process';
import { spawn, type ChildProcess } from 'node:child_process';
import { createHash } from 'node:crypto';
import { dirname, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createLogger, type Logger } from '@waggle/hive-mind-shim-core';
import { resolvePaths, type ResolvePathsOptions } from './paths.js';
import { normalizeCliPath } from '@waggle/hive-mind-hooks-core';
import { HIVE_HOOK_ENTRY_KEY, resolvePaths, type ResolvePathsOptions } from './paths.js';
import { parseConfig, hasHiveEntries, HOOKS_KEY } from './json5-merger.js';
import {
OPENCLAW_HANDLER_BUNDLE,
OPENCLAW_HANDLER_PACKAGE_JSON,
renderOpenclawHandlerEntrySource,
type OpenclawRuntimeBinding,
} from './install.js';
export interface VerifyCheck {
name: string;
@@ -31,10 +39,16 @@ export interface VerifyResult {
export interface VerifyOptions extends ResolvePathsOptions {
logger?: Logger;
/** Override hive-mind-cli executable name. Default 'hive-mind-cli'. */
/** Authoritative packaged CLI path expected by the launcher/verifier. */
cliPath?: string;
/** Expected packaged Node path; production receives this from the launcher runtime. */
nodePath?: string;
/** Fail closed unless a complete packaged Node/CLI binding is present. */
requireManagedRuntime?: boolean;
/** Test hook for spawn. */
spawnImpl?: typeof spawn;
/** Test hook for the CLI probe timeout. */
cliProbeTimeoutMs?: number;
}
async function fileReadable(p: string): Promise<boolean> {
@@ -53,43 +67,222 @@ function internalEnabled(config: Record<string, unknown>): boolean {
return (internal as Record<string, unknown>)['enabled'] === true;
}
function probeCliVersion(
cliPath: string,
spawnImpl: typeof spawn,
function configuredCliPath(config: Record<string, unknown>): string | undefined {
const hooks = config[HOOKS_KEY];
if (!hooks || typeof hooks !== 'object') return undefined;
const internal = (hooks as Record<string, unknown>)['internal'];
if (!internal || typeof internal !== 'object') return undefined;
const entries = (internal as Record<string, unknown>)['entries'];
if (!entries || typeof entries !== 'object') return undefined;
const hiveEntry = (entries as Record<string, unknown>)[HIVE_HOOK_ENTRY_KEY];
if (!hiveEntry || typeof hiveEntry !== 'object') return undefined;
const env = (hiveEntry as Record<string, unknown>)['env'];
if (!env || typeof env !== 'object') return undefined;
const cliPath = (env as Record<string, unknown>)['WAGGLE_HIVE_MIND_CLI'];
return typeof cliPath === 'string' && cliPath.length > 0 ? cliPath : undefined;
}
function configuredNodePath(config: Record<string, unknown>): string | undefined {
const hooks = config[HOOKS_KEY];
if (!hooks || typeof hooks !== 'object') return undefined;
const internal = (hooks as Record<string, unknown>)['internal'];
if (!internal || typeof internal !== 'object') return undefined;
const entries = (internal as Record<string, unknown>)['entries'];
if (!entries || typeof entries !== 'object') return undefined;
const hiveEntry = (entries as Record<string, unknown>)[HIVE_HOOK_ENTRY_KEY];
if (!hiveEntry || typeof hiveEntry !== 'object') return undefined;
const env = (hiveEntry as Record<string, unknown>)['env'];
if (!env || typeof env !== 'object') return undefined;
const nodePath = (env as Record<string, unknown>)['WAGGLE_HOOK_NODE_PATH'];
return typeof nodePath === 'string' && nodePath.length > 0 ? nodePath : undefined;
}
const TERMINATION_GRACE_MS = 250;
function waitForProbe(
child: ChildProcess,
timeoutMs: number,
timeoutMessage: string,
successMessage: string,
outputLimit: number,
): Promise<{ ok: boolean; output: string }> {
return new Promise((resolve) => {
let settled = false;
const command = isJsPath(cliPath) ? process.execPath : cliPath;
const args = isJsPath(cliPath) ? [cliPath, '--help'] : ['--help'];
const child = spawnImpl(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
let timedOut = false;
const timers: {
timeout?: ReturnType<typeof setTimeout>;
force?: ReturnType<typeof setTimeout>;
detach?: ReturnType<typeof setTimeout>;
} = {};
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
const timer = setTimeout(() => {
const clearTimers = (): void => {
if (timers.timeout !== undefined) clearTimeout(timers.timeout);
if (timers.force !== undefined) clearTimeout(timers.force);
if (timers.detach !== undefined) clearTimeout(timers.detach);
};
const finish = (result: { ok: boolean; output: string }): void => {
if (settled) return;
settled = true;
try { child.kill('SIGTERM'); } catch { /* gone */ }
resolve({ ok: false, output: 'timed out probing hive-mind-cli' });
}, timeoutMs);
child.stdout?.on('data', (c: Buffer) => stdout.push(c));
child.stderr?.on('data', (c: Buffer) => stderr.push(c));
child.on('error', (err) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({ ok: false, output: err instanceof Error ? err.message : String(err) });
clearTimers();
resolve(result);
};
child.stdout?.on('data', (chunk: Buffer) => stdout.push(chunk));
child.stderr?.on('data', (chunk: Buffer) => stderr.push(chunk));
child.on('error', (error) => {
finish({
ok: false,
output: timedOut ? timeoutMessage : error instanceof Error ? error.message : String(error),
});
});
child.on('exit', (code) => {
if (settled) return;
settled = true;
clearTimeout(timer);
const out = Buffer.concat(stdout).toString('utf-8').slice(0, 200);
const err = Buffer.concat(stderr).toString('utf-8').slice(0, 200);
resolve({ ok: code === 0, output: code === 0 ? out : err });
if (timedOut) {
finish({ ok: false, output: timeoutMessage });
return;
}
const output = Buffer.concat(code === 0 ? stdout : stderr)
.toString('utf-8')
.trim()
.slice(0, outputLimit);
finish({
ok: code === 0,
output: output || (code === 0 ? successMessage : `process exited ${String(code)}`),
});
});
timers.timeout = setTimeout(() => {
if (settled) return;
timedOut = true;
try { child.kill('SIGTERM'); } catch { /* already gone */ }
timers.force = setTimeout(() => {
if (settled) return;
try { child.kill('SIGKILL'); } catch { /* already gone */ }
timers.detach = setTimeout(() => {
if (settled) return;
child.stdout?.destroy();
child.stderr?.destroy();
if (typeof child.unref === 'function') child.unref();
finish({ ok: false, output: `${timeoutMessage}; process did not exit after SIGKILL` });
}, TERMINATION_GRACE_MS);
}, TERMINATION_GRACE_MS);
}, timeoutMs);
});
}
function probeCliVersion(
cliPath: string,
nodePath: string | undefined,
spawnImpl: typeof spawn,
timeoutMs: number,
): Promise<{ ok: boolean; output: string }> {
const command = isJsPath(cliPath) ? nodePath ?? process.execPath : cliPath;
const args = isJsPath(cliPath) ? [cliPath, '--help'] : ['--help'];
const child = spawnImpl(command, args, {
env: runtimeProbeEnv(),
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
return waitForProbe(
child,
timeoutMs,
'timed out probing hive-mind-cli',
'hive-mind-cli exited successfully',
200,
);
}
async function sha256File(filePath: string): Promise<string | undefined> {
try {
return createHash('sha256').update(await readFile(filePath)).digest('hex');
} catch {
return undefined;
}
}
async function fileHasExactText(filePath: string, expected: string): Promise<boolean> {
try {
return await readFile(filePath, 'utf-8') === expected;
} catch {
return false;
}
}
function runtimeBindingFromPointer(pointer: Record<string, unknown> | undefined): {
declared: boolean;
binding?: OpenclawRuntimeBinding;
} {
const extra = pointer?.['extra'];
if (!extra || typeof extra !== 'object') return { declared: false };
const candidate = (extra as Record<string, unknown>)['runtime_binding'];
if (candidate === undefined || candidate === null) return { declared: false };
if (!candidate || typeof candidate !== 'object') return { declared: true };
const value = candidate as Record<string, unknown>;
const sha256 = /^[a-f0-9]{64}$/;
if (
value['version'] !== 1
|| typeof value['node_path'] !== 'string'
|| value['node_path'].length === 0
|| typeof value['node_sha256'] !== 'string'
|| !sha256.test(value['node_sha256'])
|| typeof value['cli_path'] !== 'string'
|| value['cli_path'].length === 0
|| typeof value['cli_sha256'] !== 'string'
|| !sha256.test(value['cli_sha256'])
) {
return { declared: true };
}
return {
declared: true,
binding: {
version: 1,
node_path: value['node_path'],
node_sha256: value['node_sha256'],
cli_path: value['cli_path'],
cli_sha256: value['cli_sha256'],
},
};
}
function runtimeProbeEnv(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { NODE_PATH: '', NO_COLOR: '1' };
for (const key of [
'PATH', 'Path', 'SystemRoot', 'SYSTEMROOT', 'WINDIR', 'ComSpec',
'PATHEXT', 'TEMP', 'TMP',
]) {
const value = process.env[key];
if (value !== undefined) env[key] = value;
}
return env;
}
function probeInstalledHandler(
handlerPath: string,
nodePath: string | undefined,
timeoutMs: number,
): Promise<{ ok: boolean; output: string }> {
const handlerUrl = pathToFileURL(handlerPath).href;
const script = [
`const loaded = await import(${JSON.stringify(handlerUrl)});`,
`if (typeof loaded.default !== 'function') throw new Error('installed handler default export is not a function');`,
`await loaded.default({ type: 'noop', action: 'noop' });`,
].join('\n');
const child = spawn(nodePath ?? process.execPath, ['--input-type=module', '--eval', script], {
cwd: dirname(handlerPath),
env: runtimeProbeEnv(),
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
return waitForProbe(
child,
timeoutMs,
'timed out runtime-loading installed handler',
'default export loaded and handled a noop event',
500,
);
}
export async function verify(opts: VerifyOptions = {}): Promise<VerifyResult> {
const log = opts.logger ?? createLogger({ name: 'openclaw-hooks/verify' });
const paths = resolvePaths({
@@ -152,25 +345,175 @@ export async function verify(opts: VerifyOptions = {}): Promise<VerifyResult> {
ok: await fileReadable(paths.installedHandlerPath),
detail: paths.installedHandlerPath,
});
// 5. hive-mind-cli responds to --help (prefer install-pinned --cli-path).
const installedBundlePath = join(paths.hiveHookDir, OPENCLAW_HANDLER_BUNDLE);
const installedPackagePath = join(paths.hiveHookDir, 'package.json');
checks.push({
name: 'handler.cjs readable on disk',
ok: await fileReadable(installedBundlePath),
detail: installedBundlePath,
});
const [installedBundleHash, trustedBundleHash] = await Promise.all([
sha256File(installedBundlePath),
sha256File(paths.handlerSourcePath),
]);
const bundleTrusted = installedBundleHash !== undefined
&& trustedBundleHash !== undefined
&& installedBundleHash === trustedBundleHash;
checks.push({
name: 'handler.cjs matches trusted bundle',
ok: bundleTrusted,
detail: bundleTrusted
? `sha256 ${installedBundleHash}`
: `installed=${installedBundleHash ?? 'unreadable'} trusted=${trustedBundleHash ?? 'unreadable'}`,
});
let pointerObj: Record<string, unknown> | undefined;
let cliPathFromPointer: string | undefined;
if (existsSync(paths.pointerPath)) {
try {
const pointerObj = JSON.parse(await readFile(paths.pointerPath, 'utf-8')) as Record<string, unknown>;
pointerObj = JSON.parse(await readFile(paths.pointerPath, 'utf-8')) as Record<string, unknown>;
const pointerCliPath = pointerObj['cli_path'];
if (typeof pointerCliPath === 'string' && pointerCliPath.length > 0) {
cliPathFromPointer = pointerCliPath;
}
} catch { /* pointer unreadable — fall through */ }
} catch { /* pointer unreadable — fail the trust comparison below */ }
}
const cliPath = opts.cliPath ?? cliPathFromPointer ?? 'hive-mind-cli';
const cliPathFromConfig = configuredCliPath(parsed);
const nodePathFromConfig = configuredNodePath(parsed);
const runtimeBindingRead = runtimeBindingFromPointer(pointerObj);
const runtimeBinding = runtimeBindingRead.binding;
const expectedCliPath = normalizeCliPath(opts.cliPath);
const launcherNodePath = opts.nodePath
?? process.env.WAGGLE_HOOK_NODE_PATH
?? process.execPath;
const runtimeBindingRequired = opts.requireManagedRuntime === true
|| expectedCliPath !== undefined
|| runtimeBindingRead.declared
|| nodePathFromConfig !== undefined
|| opts.nodePath !== undefined
|| process.env.WAGGLE_HOOK_NODE_PATH !== undefined;
const runtimePathsAgree = !runtimeBindingRequired || (
runtimeBinding !== undefined
&& runtimeBinding.cli_path === cliPathFromPointer
&& runtimeBinding.cli_path === cliPathFromConfig
&& runtimeBinding.node_path === nodePathFromConfig
&& runtimeBinding.node_path === launcherNodePath
);
checks.push({
name: 'packaged Node matches verifier expectation',
ok: runtimePathsAgree,
detail: runtimeBindingRequired
? `binding=${runtimeBinding?.node_path ?? '(invalid)'} config=${nodePathFromConfig ?? '(none)'} verifier=${launcherNodePath}`
: 'legacy install without a packaged runtime binding',
});
const cliExpectationMatches = expectedCliPath === undefined
? opts.requireManagedRuntime !== true
: runtimeBinding !== undefined && runtimeBinding.cli_path === expectedCliPath;
checks.push({
name: 'packaged CLI matches verifier expectation',
ok: cliExpectationMatches,
detail: expectedCliPath === undefined
? opts.requireManagedRuntime === true
? 'managed verification requires --cli-path'
: 'no authoritative packaged CLI expectation'
: `binding=${runtimeBinding?.cli_path ?? '(invalid)'} verifier=${expectedCliPath}`,
});
const [currentNodeHash, currentCliHash] = runtimeBinding === undefined
? [undefined, undefined]
: await Promise.all([
sha256File(runtimeBinding.node_path),
sha256File(runtimeBinding.cli_path),
]);
const nodeHashTrusted = !runtimeBindingRequired || (
runtimeBinding !== undefined
&& currentNodeHash !== undefined
&& currentNodeHash === runtimeBinding.node_sha256
);
const cliHashTrusted = !runtimeBindingRequired || (
runtimeBinding !== undefined
&& currentCliHash !== undefined
&& currentCliHash === runtimeBinding.cli_sha256
);
checks.push({
name: 'packaged Node matches install receipt',
ok: nodeHashTrusted,
detail: runtimeBindingRequired
? `installed=${runtimeBinding?.node_sha256 ?? '(invalid)'} current=${currentNodeHash ?? '(unreadable)'}`
: 'legacy install without a packaged runtime binding',
});
checks.push({
name: 'packaged CLI matches install receipt',
ok: cliHashTrusted,
detail: runtimeBindingRequired
? `installed=${runtimeBinding?.cli_sha256 ?? '(invalid)'} current=${currentCliHash ?? '(unreadable)'}`
: 'legacy install without a packaged runtime binding',
});
// These digests are point-in-time drift receipts, not authentication against
// the local account that owns both the current-user app and OpenClaw config.
const runtimeBindingTrusted = runtimePathsAgree
&& cliExpectationMatches
&& nodeHashTrusted
&& cliHashTrusted;
const pointerCliPathTrusted = cliPathFromPointer === cliPathFromConfig
&& (!runtimeBindingRequired || runtimeBinding?.cli_path === cliPathFromPointer);
checks.push({
name: 'install pointer cli_path matches managed config',
ok: pointerCliPathTrusted,
detail: pointerCliPathTrusted
? cliPathFromPointer ?? 'no pinned CLI path'
: `pointer=${cliPathFromPointer ?? '(none)'} config=${cliPathFromConfig ?? '(none)'}`,
});
const trustedPointerCliPath = pointerCliPathTrusted && runtimeBindingTrusted
? cliPathFromPointer
: undefined;
const trustedNodePath = runtimeBindingRequired && runtimeBindingTrusted
? runtimeBinding?.node_path
: undefined;
const entryTrusted = await fileHasExactText(
paths.installedHandlerPath,
renderOpenclawHandlerEntrySource(trustedPointerCliPath, trustedNodePath),
);
checks.push({
name: 'handler.js matches managed loader',
ok: entryTrusted,
});
const packageTrusted = await fileHasExactText(
installedPackagePath,
OPENCLAW_HANDLER_PACKAGE_JSON,
);
checks.push({
name: 'hook package locks CommonJS mode',
ok: packageTrusted,
detail: installedPackagePath,
});
const artifactsTrusted = bundleTrusted
&& entryTrusted
&& packageTrusted
&& runtimeBindingTrusted;
const handlerProbe = artifactsTrusted
? await probeInstalledHandler(paths.installedHandlerPath, trustedNodePath, 4000)
: { ok: false, output: 'skipped: installed handler artifacts do not match trusted bytes' };
checks.push({
name: 'installed handler runtime-loads',
ok: handlerProbe.ok,
detail: handlerProbe.output,
});
// 5. hive-mind-cli responds to --help (prefer the trusted install pin).
const cliPath = trustedPointerCliPath ?? 'hive-mind-cli';
const spawnImpl = opts.spawnImpl ?? spawn;
const probe = await probeCliVersion(cliPath, spawnImpl, 4000);
const probe = runtimeBindingRequired && !runtimeBindingTrusted
? { ok: false, output: 'skipped: packaged Node/CLI runtime binding is not trusted' }
: await probeCliVersion(
cliPath,
trustedNodePath,
spawnImpl,
opts.cliProbeTimeoutMs ?? 4000,
);
checks.push({
name: 'hive-mind-cli reachable',
ok: probe.ok,
detail: cliPathFromPointer ? `${probe.output} (pinned: ${cliPath})` : probe.output,
detail: trustedPointerCliPath ? `${probe.output} (pinned: ${cliPath})` : probe.output,
});
const ok = checks.every((c) => c.ok);

View File

@@ -1,4 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import { copyFile, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createLogger } from '@waggle/hive-mind-shim-core';
import type {
CliBridge,
@@ -268,4 +271,95 @@ describe('openclawHook default export — fail-open over the live bridge path',
const event = { type: 'agent', action: 'bootstrap', context: { channelId: 'c', bootstrapFiles: [] } };
await expect(openclawHook(event)).resolves.toBeUndefined();
});
it('recalls history through the loader-pinned CLI and Node runtime', async () => {
const root = await mkdtemp(join(tmpdir(), 'hmocl-handler-runtime-'));
const cliPath = join(root, 'fake-hive-mind-cli.mjs');
const nodePath = join(root, 'waggle-node.exe');
const markerPath = join(root, 'cli-calls.txt');
const runtimePathReceipt = join(root, 'node-paths.txt');
const previousCliPath = process.env['WAGGLE_HIVE_MIND_CLI'];
await copyFile(process.execPath, nodePath);
await writeFile(
cliPath,
[
`import { appendFileSync } from 'node:fs';`,
`appendFileSync(${JSON.stringify(markerPath)}, process.argv.slice(2).join(' ') + '\\n');`,
`appendFileSync(${JSON.stringify(runtimePathReceipt)}, process.execPath + '\\n');`,
`const tool = process.argv[3];`,
`const payload = tool === 'recall_memory' ? [{ id: 7, content: 'historical decision only', importance: 'important', source: 'openclaw', score: 1, created_at: '2026-07-26T10:00:00.000Z' }] : { id: 'runtime-pinned-1', workspace: 'personal' };`,
`process.stdout.write(JSON.stringify({`,
` ok: true,`,
` tool,`,
` content: [{`,
` type: 'text',`,
` text: JSON.stringify(payload),`,
` }],`,
`}));`,
'',
].join('\n'),
'utf-8',
);
delete process.env['WAGGLE_HIVE_MIND_CLI'];
try {
const { default: openclawHook } = await import('../src/handler.js');
await openclawHook(
{
type: 'message',
action: 'received',
sessionKey: 'agent:main:runtime-channel',
context: {
channelId: 'runtime-channel',
content: 'persist through the install-pinned CLI',
cwd: root,
},
},
{ cliPath, nodePath },
);
const callsAfterMessage = (await readFile(markerPath, 'utf-8')).trim().split(/\r?\n/);
expect(callsAfterMessage).toHaveLength(2);
expect(callsAfterMessage[0]).toMatch(/^hook-call recall_memory /);
expect(callsAfterMessage[1]).toMatch(/^hook-call save_memory /);
const runtimePaths = (await readFile(runtimePathReceipt, 'utf-8'))
.trim()
.split(/\r?\n/)
.map((value) => value.toLowerCase());
expect(runtimePaths).toEqual([
nodePath.toLowerCase(),
nodePath.toLowerCase(),
]);
expect(runtimePaths).not.toContain(process.execPath.toLowerCase());
const bootstrapFiles: unknown[] = [];
await openclawHook(
{
type: 'agent',
action: 'bootstrap',
sessionKey: 'agent:main:runtime-channel',
context: {
channelId: 'runtime-channel',
bootstrapFiles,
},
},
{ cliPath, nodePath },
);
const callsAfterBootstrap = (await readFile(markerPath, 'utf-8')).trim().split(/\r?\n/);
expect(callsAfterBootstrap).toHaveLength(2);
expect(bootstrapFiles).toEqual([
expect.objectContaining({
name: 'HIVE_MIND_RECALL.md',
content: expect.stringContaining('historical decision only'),
}),
]);
expect(JSON.stringify(bootstrapFiles)).not.toContain('persist through the install-pinned CLI');
} finally {
if (previousCliPath === undefined) delete process.env['WAGGLE_HIVE_MIND_CLI'];
else process.env['WAGGLE_HIVE_MIND_CLI'] = previousCliPath;
await rm(root, { recursive: true, force: true });
}
});
});

View File

@@ -11,7 +11,7 @@ import { HIVE_ENTRY_KEY, HOOKS_KEY } from '../src/json5-merger.js';
interface TestEnv {
home: string;
/** A fake compiled handler.js the installer COPIES into the managed hook dir. */
/** A fake compiled CommonJS handler bundle the installer COPIES into the managed hook dir. */
handlerSource: string;
configPath: string;
pointerPath: string;
@@ -30,8 +30,8 @@ async function bootstrap(initial: string | undefined): Promise<TestEnv> {
// Fake compiled handler — install copies this verbatim into the hook dir.
const distDir = join(home, 'fake-dist');
await mkdir(distDir, { recursive: true });
const handlerSource = join(distDir, 'handler.js');
await writeFile(handlerSource, 'export default async () => {};\n', 'utf-8');
const handlerSource = join(distDir, 'handler.bundle.cjs');
await writeFile(handlerSource, 'module.exports = async () => {};\n', 'utf-8');
return {
home,
handlerSource,
@@ -146,15 +146,20 @@ describe('install (openclaw)', () => {
// ── managed hook DIR (in-process model — no per-event scripts) ─────────
it('writes the managed hook DIR with HOOK.md + a byte-identical copy of handler.js', async () => {
it('writes a host-discoverable loader plus a byte-identical .cjs bundle', async () => {
env = await bootstrap(undefined);
const handlerBytes = await readFile(env.handlerSource, 'utf-8');
const result = await install({ home: env.home, handlerSourcePath: env.handlerSource });
expect(result.hookDir).toBe(env.hiveHookDir);
expect(existsSync(join(env.hiveHookDir, 'HOOK.md'))).toBe(true);
expect(existsSync(join(env.hiveHookDir, 'handler.js'))).toBe(true);
// handler.js is copied verbatim from dist.
expect(await readFile(join(env.hiveHookDir, 'handler.js'), 'utf-8')).toBe(handlerBytes);
expect(existsSync(join(env.hiveHookDir, 'handler.cjs'))).toBe(true);
expect(await readFile(join(env.hiveHookDir, 'handler.cjs'), 'utf-8')).toBe(handlerBytes);
expect(await readFile(join(env.hiveHookDir, 'handler.js'), 'utf-8')).toContain("require('./handler.cjs')");
expect(JSON.parse(await readFile(join(env.hiveHookDir, 'package.json'), 'utf-8'))).toMatchObject({
type: 'commonjs',
private: true,
});
// HOOK.md declares the four events incl. the prefixed compaction key.
const hookMd = await readFile(join(env.hiveHookDir, 'HOOK.md'), 'utf-8');
expect(hookMd).toContain('agent:bootstrap');
@@ -168,8 +173,9 @@ describe('install (openclaw)', () => {
// saturate the CPU (forks pool, 4 workers) and the spawns can exceed vitest's
// 30s default testTimeout (observed 2026-07-15 full-suite flake,
// standalone-green). 60s per-test timeout, same class as f322cc2c.
it('copies a self-contained handler that imports and runs with NODE_PATH empty', async () => {
it('loads below a type:module ancestor and runs with NODE_PATH empty', async () => {
env = await bootstrap(undefined);
await writeFile(join(env.home, 'package.json'), '{"type":"module"}\n', 'utf-8');
const buildScript = fileURLToPath(new URL('../scripts/build-handler.mjs', import.meta.url));
const handlerEntry = fileURLToPath(new URL('../src/handler.ts', import.meta.url));
execFileSync(process.execPath, [buildScript, handlerEntry, env.handlerSource], {

View File

@@ -1,11 +1,14 @@
import { describe, expect, it, vi, afterEach } from 'vitest';
import { EventEmitter } from 'node:events';
import { Readable } from 'node:stream';
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
import { copyFile, mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { ChildProcess } from 'node:child_process';
import type { ChildProcess, SpawnOptions } from 'node:child_process';
import { install } from '../src/install.js';
import { createRecallBootstrapFile } from '../src/handler.js';
import { verify } from '../src/verify.js';
interface TestEnv {
@@ -24,8 +27,8 @@ async function bootstrap(initial: string | undefined): Promise<TestEnv> {
}
const distDir = join(home, 'fake-dist');
await mkdir(distDir, { recursive: true });
const handlerSource = join(distDir, 'handler.js');
await writeFile(handlerSource, 'export default async () => {};\n', 'utf-8');
const handlerSource = join(distDir, 'handler.bundle.cjs');
await writeFile(handlerSource, 'module.exports = async () => {};\n', 'utf-8');
return { home, handlerSource, configPath };
}
@@ -78,6 +81,7 @@ describe('verify (openclaw)', () => {
it('passes after install — entry present, subsystem enabled, dir+HOOK.md+handler on disk, CLI reachable', async () => {
const env = await bootstrap('{ "model": "opus", "hooks": {} }');
envs.push(env);
await writeFile(join(env.home, 'package.json'), '{"type":"module"}\n', 'utf-8');
await install({ home: env.home, handlerSourcePath: env.handlerSource });
const result = await verify({
home: env.home,
@@ -90,9 +94,80 @@ describe('verify (openclaw)', () => {
expect(result.checks.find((c) => c.name === 'managed hook dir exists')?.ok).toBe(true);
expect(result.checks.find((c) => c.name === 'HOOK.md readable on disk')?.ok).toBe(true);
expect(result.checks.find((c) => c.name === 'handler.js readable on disk')?.ok).toBe(true);
expect(result.checks.find((c) => c.name === 'handler.cjs readable on disk')?.ok).toBe(true);
expect(result.checks.find((c) => c.name === 'handler.cjs matches trusted bundle')?.ok).toBe(true);
expect(result.checks.find((c) => c.name === 'handler.js matches managed loader')?.ok).toBe(true);
expect(result.checks.find((c) => c.name === 'hook package locks CommonJS mode')?.ok).toBe(true);
expect(result.checks.find((c) => c.name === 'installed handler runtime-loads')?.ok).toBe(true);
expect(result.checks.find((c) => c.name === 'hive-mind-cli reachable')?.ok).toBe(true);
});
it('rejects a tampered .cjs bundle without executing its side effect', async () => {
const env = await bootstrap('{ "hooks": {} }');
envs.push(env);
await install({ home: env.home, handlerSourcePath: env.handlerSource });
const sideEffectPath = join(env.home, 'tampered-handler-executed.txt');
await writeFile(
join(env.home, '.openclaw', 'hooks', 'hive-mind', 'handler.cjs'),
[
"const { writeFileSync } = require('node:fs');",
`writeFileSync(${JSON.stringify(sideEffectPath)}, 'executed');`,
'module.exports = async () => {};',
'',
].join('\n'),
'utf-8',
);
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
spawnImpl: mockSpawnImpl({ exitCode: 0 }),
});
expect(result.ok).toBe(false);
expect(result.checks.find((c) => c.name === 'handler.cjs matches trusted bundle')?.ok).toBe(false);
const runtimeCheck = result.checks.find((c) => c.name === 'installed handler runtime-loads');
expect(runtimeCheck?.ok).toBe(false);
expect(runtimeCheck?.detail).toContain('skipped');
expect(existsSync(sideEffectPath)).toBe(false);
});
it('runtime-loads trusted code without forwarding provider API secrets', async () => {
const env = await bootstrap('{ "hooks": {} }');
envs.push(env);
const receiptPath = join(env.home, 'runtime-env.json');
await writeFile(env.handlerSource, [
"const { writeFileSync } = require('node:fs');",
'module.exports = async () => {',
` writeFileSync(${JSON.stringify(receiptPath)}, JSON.stringify({`,
' openrouter: process.env.OPENROUTER_API_KEY ?? null,',
' anthropic: process.env.ANTHROPIC_API_KEY ?? null,',
' }));',
'};',
'',
].join('\n'), 'utf-8');
await install({ home: env.home, handlerSourcePath: env.handlerSource });
const previousOpenRouter = process.env['OPENROUTER_API_KEY'];
const previousAnthropic = process.env['ANTHROPIC_API_KEY'];
process.env['OPENROUTER_API_KEY'] = 'must-not-reach-runtime-probe';
process.env['ANTHROPIC_API_KEY'] = 'must-not-reach-runtime-probe';
try {
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
spawnImpl: mockSpawnImpl({ exitCode: 0 }),
});
expect(result.ok).toBe(true);
expect(JSON.parse(await readFile(receiptPath, 'utf-8'))).toEqual({
openrouter: null,
anthropic: null,
});
} finally {
if (previousOpenRouter === undefined) delete process.env['OPENROUTER_API_KEY'];
else process.env['OPENROUTER_API_KEY'] = previousOpenRouter;
if (previousAnthropic === undefined) delete process.env['ANTHROPIC_API_KEY'];
else process.env['ANTHROPIC_API_KEY'] = previousAnthropic;
}
});
it('flags the activation advisory (FAIL) when internal.enabled is false even with the entry present', async () => {
// Entry present but subsystem OFF — hooks are inert until opted in (§5.5/§6.2).
const config = '{ "hooks": { "internal": { "enabled": false, "entries": { "hive-mind": { "enabled": true } } } } }';
@@ -147,24 +222,407 @@ describe('verify (openclaw)', () => {
const cliPath = '/abs/from/pointer.js';
await install({ home: env.home, handlerSourcePath: env.handlerSource, cliPath });
const records: Array<{ command: string; args: readonly string[] }> = [];
const recordingSpawn = ((cmd: string, args: readonly string[]) => {
records.push({ command: cmd, args });
const records: Array<{
command: string;
args: readonly string[];
options?: { env?: NodeJS.ProcessEnv; windowsHide?: boolean };
}> = [];
const recordingSpawn = ((cmd: string, args: readonly string[], options?: SpawnOptions) => {
records.push({
command: cmd,
args,
options,
});
return mockSpawnImpl({ exitCode: 0 })(cmd, args);
}) as typeof import('node:child_process').spawn;
}) as unknown as typeof import('node:child_process').spawn;
const previousOpenRouter = process.env['OPENROUTER_API_KEY'];
const previousAnthropic = process.env['ANTHROPIC_API_KEY'];
process.env['OPENROUTER_API_KEY'] = 'must-not-reach-cli-probe';
process.env['ANTHROPIC_API_KEY'] = 'must-not-reach-cli-probe';
try {
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
spawnImpl: recordingSpawn,
});
expect(result.ok).toBe(true);
const probeRecord = records[records.length - 1];
expect(probeRecord.command).toBe(process.execPath);
expect(probeRecord.args[0]).toBe(cliPath);
expect(probeRecord.args[1]).toBe('--help');
expect(probeRecord.options?.env?.['OPENROUTER_API_KEY']).toBeUndefined();
expect(probeRecord.options?.env?.['ANTHROPIC_API_KEY']).toBeUndefined();
expect(probeRecord.options?.windowsHide).toBe(true);
// Sanity: the pinned cli path was actually written into the pointer.
const pointer = JSON.parse(await readFile(join(env.home, '.openclaw', 'hive-mind-install.json'), 'utf-8')) as Record<string, unknown>;
expect(pointer['cli_path']).toBe(cliPath);
} finally {
if (previousOpenRouter === undefined) delete process.env['OPENROUTER_API_KEY'];
else process.env['OPENROUTER_API_KEY'] = previousOpenRouter;
if (previousAnthropic === undefined) delete process.env['ANTHROPIC_API_KEY'];
else process.env['ANTHROPIC_API_KEY'] = previousAnthropic;
}
});
it('uses the install-pinned Node runtime for a JavaScript CLI probe', async () => {
const env = await bootstrap(undefined);
envs.push(env);
const cliPath = join(env.home, 'hive-mind-cli.js');
const nodePath = join(env.home, 'waggle-node.exe');
await writeFile(cliPath, 'console.log("hive-mind-cli");\n', 'utf-8');
await copyFile(process.execPath, nodePath);
await install({
home: env.home,
handlerSourcePath: env.handlerSource,
cliPath,
nodePath,
});
const pointer = JSON.parse(
await readFile(join(env.home, '.openclaw', 'hive-mind-install.json'), 'utf-8'),
) as Record<string, unknown>;
const binding = (pointer['extra'] as Record<string, unknown>)['runtime_binding'] as Record<string, unknown>;
expect(binding).toMatchObject({
version: 1,
node_path: nodePath,
cli_path: cliPath,
node_sha256: createHash('sha256').update(await readFile(nodePath)).digest('hex'),
cli_sha256: createHash('sha256').update(await readFile(cliPath)).digest('hex'),
});
const config = JSON.parse(await readFile(env.configPath, 'utf-8')) as {
hooks: { internal: { entries: { 'hive-mind': { env: Record<string, string> } } } };
};
expect(config.hooks.internal.entries['hive-mind'].env['WAGGLE_HOOK_NODE_PATH']).toBe(nodePath);
const probes: Array<{ command: string; args: readonly string[] }> = [];
const recordingSpawn = ((command: string, args: readonly string[]) => {
probes.push({ command, args });
return mockSpawnImpl({ exitCode: 0 })(command, args);
}) as unknown as typeof import('node:child_process').spawn;
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
nodePath,
spawnImpl: recordingSpawn,
});
expect(result.checks.find((c) => c.name === 'packaged Node matches verifier expectation')?.ok).toBe(true);
expect(result.checks.find((c) => c.name === 'packaged Node matches install receipt')?.ok).toBe(true);
expect(probes.at(-1)).toEqual({
command: nodePath,
args: [cliPath, '--help'],
});
});
it.each(['node', 'cli'] as const)(
'rejects changed pinned %s bytes without spawning the managed runtime',
async (artifact) => {
const env = await bootstrap(undefined);
envs.push(env);
const cliPath = join(env.home, 'hive-mind-cli.js');
const nodePath = join(env.home, 'waggle-node.exe');
await writeFile(cliPath, 'console.log("hive-mind-cli");\n', 'utf-8');
await copyFile(process.execPath, nodePath);
await install({
home: env.home,
handlerSourcePath: env.handlerSource,
cliPath,
nodePath,
});
await writeFile(
artifact === 'node' ? nodePath : cliPath,
`tampered-${artifact}`,
'utf-8',
);
const probes: Array<{ command: string; args: readonly string[] }> = [];
const recordingSpawn = ((command: string, args: readonly string[]) => {
probes.push({ command, args });
return mockSpawnImpl({ exitCode: 0 })(command, args);
}) as unknown as typeof import('node:child_process').spawn;
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
nodePath,
spawnImpl: recordingSpawn,
});
const digestCheck = artifact === 'node'
? 'packaged Node matches install receipt'
: 'packaged CLI matches install receipt';
expect(result.checks.find((check) => check.name === digestCheck)?.ok).toBe(false);
expect(result.checks.find((check) => check.name === 'installed handler runtime-loads')?.detail)
.toContain('skipped');
expect(result.checks.find((check) => check.name === 'hive-mind-cli reachable')?.detail)
.toContain('skipped');
expect(probes).toEqual([]);
},
);
it('rejects a verifier CLI expectation that differs from the loader pin', async () => {
const env = await bootstrap('{ "hooks": {} }');
envs.push(env);
const pinnedCliPath = '/abs/broken-pinned-cli.js';
const overrideCliPath = '/abs/working-override-cli.js';
await install({
home: env.home,
handlerSourcePath: env.handlerSource,
cliPath: pinnedCliPath,
});
const invokedPaths: string[] = [];
const recordingSpawn = ((
cmd: string,
args: readonly string[],
) => {
const invokedPath = cmd === process.execPath ? args[0] : cmd;
if (invokedPath !== undefined) invokedPaths.push(invokedPath);
return mockSpawnImpl({
exitCode: invokedPath === pinnedCliPath ? 127 : 0,
stderr: invokedPath === pinnedCliPath ? 'not found' : '',
})(cmd, args);
}) as unknown as typeof import('node:child_process').spawn;
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
cliPath: overrideCliPath,
spawnImpl: recordingSpawn,
});
expect(result.ok).toBe(false);
expect(result.checks.find((c) => c.name === 'hive-mind-cli reachable')?.ok).toBe(false);
expect(result.checks.find((c) => c.name === 'packaged CLI matches verifier expectation')?.ok)
.toBe(false);
expect(invokedPaths).not.toContain(pinnedCliPath);
expect(invokedPaths).not.toContain(overrideCliPath);
});
it('rejects managed verification when the loader has no runtime pin', async () => {
const env = await bootstrap('{ "hooks": {} }');
envs.push(env);
const overrideCliPath = '/abs/working-override-cli.js';
await install({
home: env.home,
handlerSourcePath: env.handlerSource,
});
const invokedPaths: string[] = [];
const recordingSpawn = ((
cmd: string,
args: readonly string[],
) => {
const invokedPath = cmd === process.execPath ? args[0] : cmd;
if (invokedPath !== undefined) invokedPaths.push(invokedPath);
return mockSpawnImpl({
exitCode: invokedPath === 'hive-mind-cli' ? 127 : 0,
stderr: invokedPath === 'hive-mind-cli' ? 'not found' : '',
})(cmd, args);
}) as unknown as typeof import('node:child_process').spawn;
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
cliPath: overrideCliPath,
spawnImpl: recordingSpawn,
});
expect(result.ok).toBe(false);
expect(result.checks.find((c) => c.name === 'hive-mind-cli reachable')?.ok).toBe(false);
expect(result.checks.find((c) => c.name === 'packaged CLI matches verifier expectation')?.ok)
.toBe(false);
expect(invokedPaths).not.toContain('hive-mind-cli');
expect(invokedPaths).not.toContain(overrideCliPath);
});
it('rejects coordinated CLI substitution against the verifier expectation', async () => {
const env = await bootstrap('{ "hooks": {} }');
envs.push(env);
const trustedCliPath = join(env.home, 'trusted-hive-mind-cli.js');
const attackerCliPath = join(env.home, 'substituted-hive-mind-cli.js');
const nodePath = join(env.home, 'waggle-node.exe');
await writeFile(trustedCliPath, 'console.log("trusted");\n', 'utf-8');
await writeFile(attackerCliPath, 'console.log("substituted");\n', 'utf-8');
await copyFile(process.execPath, nodePath);
await install({
home: env.home,
handlerSourcePath: env.handlerSource,
cliPath: trustedCliPath,
nodePath,
});
const pointerPath = join(env.home, '.openclaw', 'hive-mind-install.json');
const pointer = JSON.parse(await readFile(pointerPath, 'utf-8')) as {
cli_path: string;
extra: { runtime_binding: Record<string, unknown> };
};
pointer.cli_path = attackerCliPath;
pointer.extra.runtime_binding['cli_path'] = attackerCliPath;
pointer.extra.runtime_binding['cli_sha256'] = createHash('sha256')
.update(await readFile(attackerCliPath))
.digest('hex');
await writeFile(pointerPath, `${JSON.stringify(pointer, null, 2)}\n`, 'utf-8');
const config = JSON.parse(await readFile(env.configPath, 'utf-8')) as {
hooks: { internal: { entries: { 'hive-mind': { env: Record<string, string> } } } };
};
config.hooks.internal.entries['hive-mind'].env['WAGGLE_HIVE_MIND_CLI'] = attackerCliPath;
await writeFile(env.configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
const handlerPath = join(env.home, '.openclaw', 'hooks', 'hive-mind', 'handler.js');
await writeFile(
handlerPath,
[
`'use strict';`,
`const handler = require('./handler.cjs');`,
`module.exports = (event) => handler(event, ${JSON.stringify({
cliPath: attackerCliPath,
nodePath,
})});`,
'',
].join('\n'),
'utf-8',
);
const probes: string[] = [];
const recordingSpawn = ((command: string) => {
probes.push(command);
return mockSpawnImpl({ exitCode: 0 })(command, []);
}) as unknown as typeof import('node:child_process').spawn;
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
cliPath: trustedCliPath,
nodePath,
spawnImpl: recordingSpawn,
});
expect(result.ok).toBe(false);
expect(result.checks.find((c) => c.name === 'packaged CLI matches verifier expectation')?.ok)
.toBe(false);
expect(probes).toEqual([]);
});
it('rejects a stripped managed binding instead of downgrading to legacy verification', async () => {
const env = await bootstrap('{ "hooks": {} }');
envs.push(env);
const cliPath = join(env.home, 'hive-mind-cli.js');
const nodePath = join(env.home, 'waggle-node.exe');
await writeFile(cliPath, 'console.log("trusted");\n', 'utf-8');
await copyFile(process.execPath, nodePath);
await install({
home: env.home,
handlerSourcePath: env.handlerSource,
cliPath,
nodePath,
});
const pointerPath = join(env.home, '.openclaw', 'hive-mind-install.json');
const pointer = JSON.parse(await readFile(pointerPath, 'utf-8')) as Record<string, unknown>;
delete pointer['cli_path'];
const extra = pointer['extra'] as Record<string, unknown>;
delete extra['runtime_binding'];
await writeFile(pointerPath, `${JSON.stringify(pointer, null, 2)}\n`, 'utf-8');
const config = JSON.parse(await readFile(env.configPath, 'utf-8')) as {
hooks: { internal: { entries: { 'hive-mind': { env: Record<string, string> } } } };
};
delete config.hooks.internal.entries['hive-mind'].env['WAGGLE_HIVE_MIND_CLI'];
delete config.hooks.internal.entries['hive-mind'].env['WAGGLE_HOOK_NODE_PATH'];
await writeFile(env.configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
const handlerPath = join(env.home, '.openclaw', 'hooks', 'hive-mind', 'handler.js');
await writeFile(
handlerPath,
"'use strict';\nmodule.exports = require('./handler.cjs');\n",
'utf-8',
);
const probes: string[] = [];
const recordingSpawn = ((command: string) => {
probes.push(command);
return mockSpawnImpl({ exitCode: 0 })(command, []);
}) as unknown as typeof import('node:child_process').spawn;
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
cliPath,
requireManagedRuntime: true,
spawnImpl: recordingSpawn,
});
expect(result.ok).toBe(false);
expect(result.checks.find((c) => c.name === 'packaged CLI matches verifier expectation')?.ok)
.toBe(false);
expect(probes).toEqual([]);
});
it('rejects a tampered pointer cli_path without spawning it', async () => {
const env = await bootstrap('{ "hooks": {} }');
envs.push(env);
const trustedCliPath = '/abs/trusted-cli.js';
await install({ home: env.home, handlerSourcePath: env.handlerSource, cliPath: trustedCliPath });
const pointerPath = join(env.home, '.openclaw', 'hive-mind-install.json');
const pointer = JSON.parse(await readFile(pointerPath, 'utf-8')) as Record<string, unknown>;
const tamperedCliPath = join(env.home, 'tampered-cli.js');
pointer['cli_path'] = tamperedCliPath;
await writeFile(pointerPath, `${JSON.stringify(pointer, null, 2)}\n`, 'utf-8');
const spawned: Array<{ command: string; args: readonly string[] }> = [];
const recordingSpawn = ((command: string, args: readonly string[], _options?: SpawnOptions) => {
spawned.push({ command, args });
return mockSpawnImpl({ exitCode: 0 })(command, args);
}) as unknown as typeof import('node:child_process').spawn;
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
spawnImpl: recordingSpawn,
});
expect(result.ok).toBe(true);
const probeRecord = records[records.length - 1];
expect(probeRecord.command).toBe(process.execPath);
expect(probeRecord.args[0]).toBe(cliPath);
expect(probeRecord.args[1]).toBe('--help');
// Sanity: the pinned cli path was actually written into the pointer.
const pointer = JSON.parse(await readFile(join(env.home, '.openclaw', 'hive-mind-install.json'), 'utf-8')) as Record<string, unknown>;
expect(pointer['cli_path']).toBe(cliPath);
expect(result.ok).toBe(false);
expect(result.checks.find((c) => c.name === 'install pointer cli_path matches managed config')?.ok).toBe(false);
expect(spawned.some(({ command, args }) => command === tamperedCliPath || args.includes(tamperedCliPath))).toBe(false);
});
it('escalates a timed-out CLI probe to SIGKILL before returning', async () => {
const env = await bootstrap('{ "hooks": {} }');
envs.push(env);
await install({ home: env.home, handlerSourcePath: env.handlerSource });
const signals: NodeJS.Signals[] = [];
const hangingSpawn = (() => {
const emitter = new EventEmitter();
const child = Object.assign(emitter, {
stdout: Readable.from([]),
stderr: Readable.from([]),
kill: vi.fn((signal: NodeJS.Signals) => {
signals.push(signal);
if (signal === 'SIGKILL') setImmediate(() => emitter.emit('exit', null, 'SIGKILL'));
return true;
}),
}) as unknown as ChildProcess;
return child;
}) as unknown as typeof import('node:child_process').spawn;
const result = await verify({
home: env.home,
handlerSourcePath: env.handlerSource,
spawnImpl: hangingSpawn,
cliProbeTimeoutMs: 10,
});
expect(result.ok).toBe(false);
expect(signals).toEqual(['SIGTERM', 'SIGKILL']);
expect(result.checks.find((c) => c.name === 'hive-mind-cli reachable')?.detail).toContain('timed out');
});
});
describe('OpenClaw 2026.6.11 bootstrap file contract', () => {
it('preserves recalled text in a path/name/content object accepted by the host sanitizer', () => {
const recalled = 'hive-mind: recalled exact text';
expect(createRecallBootstrapFile(recalled)).toEqual({
path: 'HIVE_MIND_RECALL.md',
name: 'HIVE_MIND_RECALL.md',
content: recalled,
});
});
});