fleet-memory/hindsight-integrations/nemoclaw/src/setup.ts
Ben d284de28c7
feat(nemoclaw): add hindsight-nemoclaw setup CLI package (#630)
* feat(nemoclaw): add hindsight-nemoclaw setup CLI package

Automates the full NemoClaw sandbox setup:
- Installs @vectorize-io/hindsight-openclaw plugin
- Configures external API mode in ~/.openclaw/openclaw.json
- Reads current openshell sandbox policy, merges Hindsight egress rule, re-applies
- Restarts the OpenClaw gateway

Options: --dry-run, --skip-policy, --skip-plugin-install
36 unit tests passing

* docs: add NEMOCLAW.md setup guide

* feat(nemoclaw): add README, docs page, and release pipeline

* revert: remove release.yml changes from nemoclaw PR
2026-03-20 15:16:55 +01:00

148 lines
5.2 KiB
TypeScript

import { execFile } from 'child_process';
import { promisify } from 'util';
import { writeFile, rm } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { randomBytes } from 'crypto';
import type { CliArgs } from './types.js';
import { readSandboxPolicy } from './policy-reader.js';
import { hasHindsightPolicy, mergeHindsightPolicy, serializePolicy } from './policy-writer.js';
import { applyPluginConfig } from './openclaw-config.js';
const execFileAsync = promisify(execFile);
function log(msg: string) {
process.stdout.write(`${msg}\n`);
}
function step(n: number, msg: string) {
log(`\n[${n}] ${msg}`);
}
async function which(bin: string): Promise<boolean> {
try {
await execFileAsync('which', [bin]);
return true;
} catch {
return false;
}
}
export async function runSetup(args: CliArgs): Promise<void> {
log('\nhindsight-nemoclaw setup');
log('─'.repeat(40));
// Step 0 — Preflight
step(0, 'Preflight checks...');
const [hasOpenshell, hasOpenclaw] = await Promise.all([which('openshell'), which('openclaw')]);
if (!hasOpenshell) {
throw new Error('`openshell` not found on PATH. Install it from https://openshell.ai');
}
if (!hasOpenclaw) {
throw new Error('`openclaw` not found on PATH. Install it from https://openclaw.ai');
}
log(' ✓ openshell found');
log(' ✓ openclaw found');
// Step 1 — Install hindsight-openclaw plugin
if (!args.skipPluginInstall) {
step(1, 'Installing @vectorize-io/hindsight-openclaw plugin...');
if (args.dryRun) {
log(' [dry-run] would run: openclaw plugins install @vectorize-io/hindsight-openclaw');
} else {
const { stdout } = await execFileAsync('openclaw', [
'plugins', 'install', '@vectorize-io/hindsight-openclaw',
]);
log(stdout.trim() || ' ✓ Plugin installed');
}
} else {
step(1, 'Skipping plugin install (--skip-plugin-install)');
}
// Step 2 — Configure ~/.openclaw/openclaw.json
step(2, 'Configuring plugin in ~/.openclaw/openclaw.json...');
const pluginConfig = {
hindsightApiUrl: args.apiUrl,
hindsightApiToken: args.apiToken,
llmProvider: 'claude-code',
dynamicBankId: false,
bankIdPrefix: args.bankPrefix,
};
if (args.dryRun) {
log(` [dry-run] would write plugin config to ~/.openclaw/openclaw.json`);
log(` config: ${JSON.stringify(pluginConfig, null, 4).split('\n').join('\n ')}`);
} else {
await applyPluginConfig(pluginConfig);
log(` ✓ Plugin config written (bank: ${args.bankPrefix}-openclaw)`);
}
// Step 3 — Apply OpenShell network policy
if (!args.skipPolicy) {
step(3, `Applying Hindsight network policy to sandbox "${args.sandbox}"...`);
const currentPolicy = await readSandboxPolicy(args.sandbox);
if (hasHindsightPolicy(currentPolicy)) {
log(' ✓ Hindsight policy already present — skipping');
} else {
const updatedPolicy = mergeHindsightPolicy(currentPolicy);
const policyYaml = serializePolicy(updatedPolicy);
if (args.dryRun) {
log(' [dry-run] would apply policy:');
log(policyYaml.split('\n').map(l => ` ${l}`).join('\n'));
} else {
const tmpFile = join(tmpdir(), `hindsight-policy-${randomBytes(6).toString('hex')}.yaml`);
try {
await writeFile(tmpFile, policyYaml, 'utf8');
const { stdout } = await execFileAsync('openshell', [
'policy', 'set', args.sandbox, '--policy', tmpFile, '--wait',
]);
log(stdout.trim() || ` ✓ Policy applied to sandbox "${args.sandbox}"`);
} finally {
await rm(tmpFile, { force: true });
}
}
}
} else {
step(3, 'Skipping policy update (--skip-policy)');
log(' Add the following block to your sandbox network_policies manually:');
log('');
log(' hindsight:');
log(' name: hindsight');
log(' endpoints:');
log(' - host: api.hindsight.vectorize.io');
log(' port: 443');
log(' protocol: rest');
log(' tls: terminate');
log(' enforcement: enforce');
log(' rules:');
log(' - allow: { method: GET, path: /** }');
log(' - allow: { method: POST, path: /** }');
log(' - allow: { method: PUT, path: /** }');
log(' binaries:');
log(' - path: /usr/local/bin/openclaw');
}
// Step 4 — Restart gateway
step(4, 'Restarting OpenClaw gateway...');
if (args.dryRun) {
log(' [dry-run] would run: openclaw gateway restart');
} else {
await execFileAsync('openclaw', ['gateway', 'restart']);
log(' ✓ Gateway restarted');
}
log('\n' + '─'.repeat(40));
log('✓ Setup complete!\n');
log(` Bank ID: ${args.bankPrefix}-openclaw`);
log(` API URL: ${args.apiUrl}`);
log('');
log(' Watch gateway logs to confirm:');
log(' grep Hindsight ~/.openclaw/logs/gateway.log | tail -5');
log(' Expected: [Hindsight] ✓ Ready (external API mode)');
log('');
log(' Test memory retention:');
log(` openclaw agent --agent main --session-id test-1 -m "My name is Ben."`);
log(` openclaw agent --agent main --session-id test-2 -m "What do you remember about me?"`);
}