moving
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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], {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user