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
This commit is contained in:
parent
93609f74ab
commit
d284de28c7
17 changed files with 2929 additions and 0 deletions
246
hindsight-docs/docs/sdks/integrations/nemoclaw.md
Normal file
246
hindsight-docs/docs/sdks/integrations/nemoclaw.md
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# NemoClaw
|
||||
|
||||
Persistent memory for [NemoClaw](https://nemoclaw.ai) sandboxed agents using [Hindsight](https://hindsight.vectorize.io).
|
||||
|
||||
NemoClaw runs [OpenClaw](https://openclaw.ai) inside an OpenShell sandbox with controlled filesystem, process, and network egress policies. The `hindsight-nemoclaw` package automates adding Hindsight memory to a sandbox in one command — no code changes required.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npx @vectorize-io/hindsight-nemoclaw setup \
|
||||
--sandbox my-assistant \
|
||||
--api-url https://api.hindsight.vectorize.io \
|
||||
--api-token <your-api-key> \
|
||||
--bank-prefix my-sandbox
|
||||
```
|
||||
|
||||
Get an API key at [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup).
|
||||
|
||||
You'll see output like:
|
||||
|
||||
```
|
||||
[0] Preflight checks...
|
||||
✓ openshell found
|
||||
✓ openclaw found
|
||||
|
||||
[1] Installing @vectorize-io/hindsight-openclaw plugin...
|
||||
✓ Plugin installed
|
||||
|
||||
[2] Configuring plugin in ~/.openclaw/openclaw.json...
|
||||
✓ Plugin config written (bank: my-sandbox-openclaw)
|
||||
|
||||
[3] Applying Hindsight network policy to sandbox "my-assistant"...
|
||||
✓ Policy version 2 submitted
|
||||
✓ Policy version 2 loaded (active version: 2)
|
||||
|
||||
[4] Restarting OpenClaw gateway...
|
||||
✓ Gateway restarted
|
||||
|
||||
✓ Setup complete!
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### The sandbox problem
|
||||
|
||||
OpenShell enforces strict network egress — every outbound endpoint must be explicitly permitted in the sandbox policy. By default, the Hindsight API (`api.hindsight.vectorize.io`) is not in that list.
|
||||
|
||||
The `hindsight-openclaw` plugin supports **external API mode**, where it skips the local daemon entirely and makes direct HTTPS calls to Hindsight Cloud. This is the natural fit for sandboxed environments: the plugin becomes a thin HTTP client, and the only sandbox change needed is one egress rule.
|
||||
|
||||
### What the setup command does
|
||||
|
||||
1. **Preflight** — verifies `openshell` and `openclaw` are installed
|
||||
2. **Install plugin** — runs `openclaw plugins install @vectorize-io/hindsight-openclaw`
|
||||
3. **Configure plugin** — writes external API mode config to `~/.openclaw/openclaw.json`
|
||||
4. **Apply policy** — reads the current sandbox policy, merges the Hindsight egress block, and re-applies via `openshell policy set`
|
||||
5. **Restart gateway** — runs `openclaw gateway restart`
|
||||
|
||||
### Memory flow
|
||||
|
||||
Once set up, the `hindsight-openclaw` plugin hooks into the OpenClaw gateway lifecycle:
|
||||
|
||||
- **`before_agent_start`** — recalls relevant memories from past sessions and injects them into context
|
||||
- **`agent_end`** — retains the conversation to the Hindsight memory bank
|
||||
|
||||
The sandbox doesn't interfere with either step — it sees the Hindsight calls as normal HTTPS egress to a permitted endpoint.
|
||||
|
||||
## CLI Reference
|
||||
|
||||
```
|
||||
hindsight-nemoclaw setup [options]
|
||||
|
||||
Options:
|
||||
--sandbox <name> NemoClaw sandbox name (required)
|
||||
--api-url <url> Hindsight API URL (required)
|
||||
--api-token <token> Hindsight API token (required)
|
||||
--bank-prefix <prefix> Memory bank prefix (default: "nemoclaw")
|
||||
--skip-policy Skip sandbox network policy update
|
||||
--skip-plugin-install Skip openclaw plugin installation
|
||||
--dry-run Preview changes without applying
|
||||
--help Show help
|
||||
```
|
||||
|
||||
Use `--dry-run` to preview all changes before applying anything. Use `--skip-policy` if you manage sandbox policies manually.
|
||||
|
||||
## Manual Setup
|
||||
|
||||
If you prefer to apply the steps yourself instead of using the CLI:
|
||||
|
||||
### 1. Install the plugin
|
||||
|
||||
```bash
|
||||
openclaw plugins install @vectorize-io/hindsight-openclaw
|
||||
```
|
||||
|
||||
### 2. Configure `~/.openclaw/openclaw.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"hindsight-openclaw": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
|
||||
"hindsightApiToken": "<your-api-key>",
|
||||
"llmProvider": "claude-code",
|
||||
"dynamicBankId": false,
|
||||
"bankIdPrefix": "my-sandbox"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`llmProvider: "claude-code"` uses the Claude Code process already present in the sandbox — no additional API key needed.
|
||||
|
||||
### 3. Add the Hindsight network policy
|
||||
|
||||
`openshell policy set` replaces the entire policy document. Export your current policy first, add the Hindsight block, then re-apply:
|
||||
|
||||
```yaml
|
||||
network_policies:
|
||||
hindsight:
|
||||
name: hindsight
|
||||
endpoints:
|
||||
- host: api.hindsight.vectorize.io
|
||||
port: 443
|
||||
protocol: rest
|
||||
tls: terminate
|
||||
enforcement: enforce
|
||||
rules:
|
||||
- allow:
|
||||
method: GET
|
||||
path: /**
|
||||
- allow:
|
||||
method: POST
|
||||
path: /**
|
||||
- allow:
|
||||
method: PUT
|
||||
path: /**
|
||||
binaries:
|
||||
- path: /usr/local/bin/openclaw
|
||||
```
|
||||
|
||||
```bash
|
||||
openshell policy set my-sandbox --policy /path/to/full-policy.yaml --wait
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `hindsightApiUrl` | string | — | Hindsight API base URL |
|
||||
| `hindsightApiToken` | string | — | API token for authentication |
|
||||
| `llmProvider` | string | auto-detect | LLM provider for memory extraction |
|
||||
| `dynamicBankId` | boolean | `false` | Isolate memory per user (`true`) or share across sessions (`false`) |
|
||||
| `bankIdPrefix` | string | `"nemoclaw"` | Prefix for the memory bank name |
|
||||
|
||||
### Bank naming
|
||||
|
||||
When `dynamicBankId: false`, all sessions write to a single bank named `{bankIdPrefix}-openclaw`. When `dynamicBankId: true`, each user gets an isolated bank — useful for multi-tenant deployments.
|
||||
|
||||
## Verifying It Works
|
||||
|
||||
After setup, check the gateway logs:
|
||||
|
||||
```bash
|
||||
tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
|
||||
```
|
||||
|
||||
On startup you should see:
|
||||
|
||||
```
|
||||
[Hindsight] Plugin loaded successfully
|
||||
[Hindsight] ✓ Using external API: https://api.hindsight.vectorize.io
|
||||
[Hindsight] External API health: {"status":"healthy","database":"connected"}
|
||||
[Hindsight] Default bank: my-sandbox-openclaw
|
||||
[Hindsight] ✓ Ready (external API mode)
|
||||
```
|
||||
|
||||
After a conversation:
|
||||
|
||||
```
|
||||
[Hindsight] before_agent_start - bank: my-sandbox-openclaw, channel: undefined/webchat
|
||||
[Hindsight Hook] agent_end triggered - bank: my-sandbox-openclaw
|
||||
[Hindsight] Retained 6 messages to bank my-sandbox-openclaw for session agent:main:...
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
|
||||
### Policy replacement is full-document
|
||||
|
||||
`openshell policy set` replaces the entire policy document. The `hindsight-nemoclaw setup` command handles this automatically. If you're applying manually, export the current policy first so existing rules aren't lost.
|
||||
|
||||
### LaunchAgent can't follow symlinks on macOS
|
||||
|
||||
On macOS, the OpenClaw gateway runs as a LaunchAgent under a restricted security context. `openclaw plugins install --link` creates a symlink the LaunchAgent can't follow — the setup command installs as a copy instead. If you see `EPERM: operation not permitted, scandir` in gateway logs, this is the cause.
|
||||
|
||||
### Memory retention is asynchronous
|
||||
|
||||
Fact extraction and entity resolution happen in the background after `retain`. If you open a new session immediately after closing one, the most recent memories may not be indexed yet — typically a few seconds.
|
||||
|
||||
### Binary-scoped egress
|
||||
|
||||
The `binaries` field in the network policy restricts the egress rule to a specific executable path. If OpenClaw updates and the binary path changes, the rule silently stops working. Check your binary path after upgrades.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin not loading
|
||||
|
||||
```bash
|
||||
openclaw plugins list | grep hindsight
|
||||
# Should show: ✓ enabled │ Hindsight Memory │ ...
|
||||
|
||||
# Reinstall
|
||||
openclaw plugins install @vectorize-io/hindsight-openclaw
|
||||
```
|
||||
|
||||
### Egress blocked
|
||||
|
||||
If calls to `api.hindsight.vectorize.io` are being blocked, check the active sandbox policy:
|
||||
|
||||
```bash
|
||||
openshell sandbox get my-assistant
|
||||
```
|
||||
|
||||
Verify the `hindsight` block is present and the `binaries` path matches your OpenClaw binary:
|
||||
|
||||
```bash
|
||||
which openclaw
|
||||
```
|
||||
|
||||
### External API not connecting
|
||||
|
||||
```bash
|
||||
tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
|
||||
|
||||
# If you see daemon startup messages instead of "Using external API",
|
||||
# the plugin config isn't being read — check ~/.openclaw/openclaw.json
|
||||
```
|
||||
196
hindsight-integrations/nemoclaw/NEMOCLAW.md
Normal file
196
hindsight-integrations/nemoclaw/NEMOCLAW.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# Using hindsight-openclaw with NemoClaw
|
||||
|
||||
This guide covers running the `hindsight-openclaw` plugin inside a [NemoClaw](https://nemoclaw.ai) sandbox. NemoClaw runs OpenClaw inside an OpenShell sandbox, so the plugin's outbound calls to `api.hindsight.vectorize.io` must be explicitly allowed in the sandbox's network egress policy.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- NemoClaw installed and a sandbox created (`nemoclaw onboard`)
|
||||
- OpenClaw installed (`brew install openclaw` or equivalent)
|
||||
- A Hindsight API key from [ui.hindsight.vectorize.io](https://ui.hindsight.vectorize.io)
|
||||
- The plugin source built (`npm run build` in this directory)
|
||||
|
||||
## Step 1: Create a Hindsight memory bank
|
||||
|
||||
Create the bank the plugin will write to. The bank ID follows the pattern `{bankIdPrefix}-openclaw` when `dynamicBankId` is false:
|
||||
|
||||
```bash
|
||||
curl -X PUT "https://api.hindsight.vectorize.io/v1/default/banks/my-sandbox-openclaw" \
|
||||
-H "Authorization: Bearer <your-hindsight-api-key>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"mission": "Memory bank for my NemoClaw sandbox."}'
|
||||
```
|
||||
|
||||
## Step 2: Install the plugin
|
||||
|
||||
Install the plugin as a copy (not a symlink) so the OpenClaw LaunchAgent can access it:
|
||||
|
||||
```bash
|
||||
# Build first if you haven't already
|
||||
npm run build
|
||||
|
||||
# Install (copy, not link — required for LaunchAgent access)
|
||||
openclaw plugins install /path/to/hindsight-integrations/openclaw
|
||||
```
|
||||
|
||||
Alternatively, install from npm:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @vectorize-io/hindsight-openclaw
|
||||
```
|
||||
|
||||
## Step 3: Configure the plugin
|
||||
|
||||
Add the plugin config to `~/.openclaw/openclaw.json` under `plugins.entries.hindsight-openclaw`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"hindsight-openclaw": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
|
||||
"hindsightApiToken": "<your-hindsight-api-key>",
|
||||
"llmProvider": "claude-code",
|
||||
"dynamicBankId": false,
|
||||
"bankIdPrefix": "my-sandbox"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Config notes:**
|
||||
|
||||
| Field | Value | Why |
|
||||
|-------|-------|-----|
|
||||
| `hindsightApiUrl` + `hindsightApiToken` | External API URL + key | Skips the local daemon; no `uvx`/`uv` required inside the sandbox |
|
||||
| `llmProvider: "claude-code"` | `"claude-code"` | Satisfies LLM detection without a separate API key — Claude Code is available in the sandbox via the `claude_code` policy |
|
||||
| `dynamicBankId: false` | `false` | All conversations write to one bank; easier to verify during testing |
|
||||
| `bankIdPrefix` | e.g. `"my-sandbox"` | Results in bank ID `my-sandbox-openclaw` |
|
||||
|
||||
> **Note:** The gateway log will say `Dynamic bank IDs disabled - using static bank: openclaw` — this is a misleading log message. The actual bank ID used at runtime correctly applies the prefix (e.g. `my-sandbox-openclaw`). You can verify by watching for `[Hindsight] Default bank: my-sandbox-openclaw` in the logs after full initialization.
|
||||
|
||||
## Step 4: Add the Hindsight network policy to the sandbox
|
||||
|
||||
The sandbox blocks all outbound traffic by default. You need to add `api.hindsight.vectorize.io` to the egress policy.
|
||||
|
||||
Get the current full policy by running `openshell sandbox get <name>` and save it to a YAML file, then add the `hindsight` block under `network_policies`:
|
||||
|
||||
```yaml
|
||||
network_policies:
|
||||
# ... your existing policies ...
|
||||
hindsight:
|
||||
name: hindsight
|
||||
endpoints:
|
||||
- host: api.hindsight.vectorize.io
|
||||
port: 443
|
||||
protocol: rest
|
||||
tls: terminate
|
||||
enforcement: enforce
|
||||
rules:
|
||||
- allow:
|
||||
method: GET
|
||||
path: /**
|
||||
- allow:
|
||||
method: POST
|
||||
path: /**
|
||||
- allow:
|
||||
method: PUT
|
||||
path: /**
|
||||
binaries:
|
||||
- path: /usr/local/bin/openclaw
|
||||
```
|
||||
|
||||
Apply it:
|
||||
|
||||
```bash
|
||||
openshell policy set <sandbox-name> --policy /path/to/full-policy.yaml --wait
|
||||
```
|
||||
|
||||
> **Important:** `openshell policy set` replaces the entire policy, not just patches it. Make sure your YAML includes all existing network policies or they will be removed.
|
||||
|
||||
Verify the policy loaded:
|
||||
|
||||
```bash
|
||||
openshell policy get <sandbox-name>
|
||||
# Should show: Status: Loaded, and version incremented
|
||||
```
|
||||
|
||||
## Step 5: Restart the OpenClaw gateway
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
Watch the logs to confirm the plugin loaded and the API is reachable:
|
||||
|
||||
```bash
|
||||
# Should see:
|
||||
# [Hindsight] Plugin loaded successfully
|
||||
# [Hindsight] ✓ Using external API: https://api.hindsight.vectorize.io
|
||||
# [Hindsight] External API health: {"status":"healthy","database":"connected"}
|
||||
# [Hindsight] Default bank: my-sandbox-openclaw
|
||||
# [Hindsight] ✓ Ready (external API mode)
|
||||
grep Hindsight ~/.openclaw/logs/gateway.log | tail -20
|
||||
```
|
||||
|
||||
## Step 6: Test
|
||||
|
||||
Send a message to the agent:
|
||||
|
||||
```bash
|
||||
openclaw agent --agent main --session-id test-1 \
|
||||
-m "My name is Ben and I work on Hindsight. I prefer detailed commit messages."
|
||||
```
|
||||
|
||||
Verify memory was retained (check logs):
|
||||
|
||||
```bash
|
||||
grep "Retained\|agent_end" ~/.openclaw/logs/gateway.log | tail -5
|
||||
# Should see: [Hindsight] Retained N messages to bank my-sandbox-openclaw for session ...
|
||||
```
|
||||
|
||||
Test recall in a new session:
|
||||
|
||||
```bash
|
||||
openclaw agent --agent main --session-id test-2 \
|
||||
-m "What do you remember about me?"
|
||||
# Should recall your name and preferences from the previous session
|
||||
```
|
||||
|
||||
You can also verify directly against the API:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://api.hindsight.vectorize.io/v1/default/banks/my-sandbox-openclaw/memories/recall" \
|
||||
-H "Authorization: Bearer <your-hindsight-api-key>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "what do you know about the user", "max_tokens": 512}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Plugin fails to load with `EPERM: operation not permitted, scandir`**
|
||||
|
||||
You used `--link` when installing. The OpenClaw LaunchAgent runs under a restricted macOS security context and cannot access `~/Documents` or other user directories by symlink. Reinstall without `--link`:
|
||||
|
||||
```bash
|
||||
openclaw plugins uninstall hindsight-openclaw
|
||||
openclaw plugins install /path/to/hindsight-integrations/openclaw # no --link
|
||||
```
|
||||
|
||||
**`[Hindsight] Failed to retain memory (HTTP 403)`**
|
||||
|
||||
The sandbox network policy is blocking the outbound call. Check that:
|
||||
1. The `hindsight` network policy block is present in your policy YAML
|
||||
2. The policy was applied and shows `Status: Loaded` (`openshell policy get <name>`)
|
||||
3. The `binaries` list includes `/usr/local/bin/openclaw`
|
||||
|
||||
**Gateway restart times out but then recovers**
|
||||
|
||||
This is normal on first restart after installing a plugin — the LaunchAgent takes a moment to reload. The gateway is healthy if `openclaw gateway status` shows `RPC probe: ok`.
|
||||
|
||||
**`openclaw agent` fails with `Pass --to, --session-id, or --agent`**
|
||||
|
||||
You need to specify a session. Use `--agent main` to use the default agent, or `--session-id <any-string>` to create a named session.
|
||||
60
hindsight-integrations/nemoclaw/README.md
Normal file
60
hindsight-integrations/nemoclaw/README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# hindsight-nemoclaw
|
||||
|
||||
One-command setup for [Hindsight](https://hindsight.vectorize.io) persistent memory on [NemoClaw](https://nemoclaw.ai) sandboxes.
|
||||
|
||||
NemoClaw runs [OpenClaw](https://openclaw.ai) inside an OpenShell sandbox with strict network egress policies. This package automates the full setup: installing the `hindsight-openclaw` plugin, configuring external API mode, merging the Hindsight egress rule into your sandbox policy, and restarting the gateway.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npx @vectorize-io/hindsight-nemoclaw setup \
|
||||
--sandbox my-assistant \
|
||||
--api-url https://api.hindsight.vectorize.io \
|
||||
--api-token <your-api-key> \
|
||||
--bank-prefix my-sandbox
|
||||
```
|
||||
|
||||
Get an API key at [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup).
|
||||
|
||||
## Documentation
|
||||
|
||||
Full setup guide, pitfalls, and troubleshooting:
|
||||
|
||||
**[NemoClaw Integration Documentation](https://vectorize.io/hindsight/sdks/integrations/nemoclaw)**
|
||||
|
||||
Or see [NEMOCLAW.md](./NEMOCLAW.md) in this directory for a step-by-step walkthrough.
|
||||
|
||||
## CLI Reference
|
||||
|
||||
```
|
||||
hindsight-nemoclaw setup [options]
|
||||
|
||||
Options:
|
||||
--sandbox <name> NemoClaw sandbox name (required)
|
||||
--api-url <url> Hindsight API URL (required)
|
||||
--api-token <token> Hindsight API token (required)
|
||||
--bank-prefix <prefix> Memory bank prefix (default: "nemoclaw")
|
||||
--skip-policy Skip sandbox network policy update
|
||||
--skip-plugin-install Skip openclaw plugin installation
|
||||
--dry-run Preview changes without applying
|
||||
--help Show help
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
1. **Preflight** — verifies `openshell` and `openclaw` are installed
|
||||
2. **Install plugin** — runs `openclaw plugins install @vectorize-io/hindsight-openclaw`
|
||||
3. **Configure plugin** — writes external API mode config to `~/.openclaw/openclaw.json`
|
||||
4. **Apply policy** — reads current sandbox policy, merges Hindsight egress rule, re-applies via `openshell policy set`
|
||||
5. **Restart gateway** — runs `openclaw gateway restart`
|
||||
|
||||
## Links
|
||||
|
||||
- [Hindsight Documentation](https://vectorize.io/hindsight)
|
||||
- [NemoClaw](https://nemoclaw.ai)
|
||||
- [OpenClaw](https://openclaw.ai)
|
||||
- [GitHub Repository](https://github.com/vectorize-io/hindsight)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
1318
hindsight-integrations/nemoclaw/package-lock.json
generated
Normal file
1318
hindsight-integrations/nemoclaw/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
51
hindsight-integrations/nemoclaw/package.json
Normal file
51
hindsight-integrations/nemoclaw/package.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "@vectorize-io/hindsight-nemoclaw",
|
||||
"version": "0.1.0",
|
||||
"description": "Setup CLI for hindsight-openclaw on NemoClaw sandboxes — installs the plugin, configures external API mode, and applies the OpenShell network policy",
|
||||
"type": "module",
|
||||
"main": "dist/cli.js",
|
||||
"bin": {
|
||||
"hindsight-nemoclaw": "dist/cli.js"
|
||||
},
|
||||
"keywords": [
|
||||
"nemoclaw",
|
||||
"openclaw",
|
||||
"memory",
|
||||
"ai",
|
||||
"agent",
|
||||
"hindsight",
|
||||
"openshell",
|
||||
"nvidia"
|
||||
],
|
||||
"author": "Vectorize <support@vectorize.io>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vectorize-io/hindsight.git",
|
||||
"directory": "hindsight-integrations/nemoclaw"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && node -e \"const f='dist/cli.js',s=require('fs');s.writeFileSync(f,'#!/usr/bin/env node\\n'+s.readFileSync(f,'utf8'));s.chmodSync(f,0o755)\"",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "vitest run src",
|
||||
"test:watch": "vitest src",
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
84
hindsight-integrations/nemoclaw/src/cli.ts
Normal file
84
hindsight-integrations/nemoclaw/src/cli.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { runSetup } from './setup.js';
|
||||
import type { CliArgs } from './types.js';
|
||||
|
||||
function usage(): void {
|
||||
process.stdout.write(`
|
||||
hindsight-nemoclaw — Setup CLI for Hindsight memory on NemoClaw sandboxes
|
||||
|
||||
Usage:
|
||||
hindsight-nemoclaw setup [options]
|
||||
|
||||
Required options:
|
||||
--sandbox <name> NemoClaw sandbox name (e.g. my-assistant)
|
||||
--api-url <url> Hindsight Cloud API URL (https://api.hindsight.vectorize.io)
|
||||
--api-token <token> Hindsight API key from https://ui.hindsight.vectorize.io
|
||||
--bank-prefix <prefix> Bank ID prefix (memories go to <prefix>-openclaw)
|
||||
|
||||
Optional options:
|
||||
--skip-policy Skip the openshell policy update
|
||||
--skip-plugin-install Skip openclaw plugins install
|
||||
--dry-run Print what would be changed without executing
|
||||
--help Show this help
|
||||
|
||||
Example:
|
||||
hindsight-nemoclaw setup \\
|
||||
--sandbox my-assistant \\
|
||||
--api-url https://api.hindsight.vectorize.io \\
|
||||
--api-token hsk_abc123 \\
|
||||
--bank-prefix my-sandbox
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): CliArgs | null {
|
||||
const args = argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
||||
usage();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args[0] !== 'setup') {
|
||||
process.stderr.write(`Unknown command: ${args[0]}\nRun with --help for usage.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const get = (flag: string): string | undefined => {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx === -1 || idx + 1 >= args.length) return undefined;
|
||||
return args[idx + 1];
|
||||
};
|
||||
|
||||
const sandbox = get('--sandbox');
|
||||
const apiUrl = get('--api-url');
|
||||
const apiToken = get('--api-token');
|
||||
const bankPrefix = get('--bank-prefix');
|
||||
|
||||
const missing: string[] = [];
|
||||
if (!sandbox) missing.push('--sandbox');
|
||||
if (!apiUrl) missing.push('--api-url');
|
||||
if (!apiToken) missing.push('--api-token');
|
||||
if (!bankPrefix) missing.push('--bank-prefix');
|
||||
|
||||
if (missing.length > 0) {
|
||||
process.stderr.write(`Missing required options: ${missing.join(', ')}\nRun with --help for usage.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
sandbox: sandbox!,
|
||||
apiUrl: apiUrl!,
|
||||
apiToken: apiToken!,
|
||||
bankPrefix: bankPrefix!,
|
||||
skipPolicy: args.includes('--skip-policy'),
|
||||
skipPluginInstall: args.includes('--skip-plugin-install'),
|
||||
dryRun: args.includes('--dry-run'),
|
||||
};
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv);
|
||||
if (args) {
|
||||
runSetup(args).catch(err => {
|
||||
process.stderr.write(`\nError: ${err instanceof Error ? err.message : String(err)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
101
hindsight-integrations/nemoclaw/src/openclaw-config.test.ts
Normal file
101
hindsight-integrations/nemoclaw/src/openclaw-config.test.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { mergePluginConfig } from './openclaw-config.js';
|
||||
import type { OpenClawConfig, HindsightPluginConfig } from './openclaw-config.js';
|
||||
|
||||
const PLUGIN_CONFIG: HindsightPluginConfig = {
|
||||
hindsightApiUrl: 'https://api.hindsight.vectorize.io',
|
||||
hindsightApiToken: 'hsk_test123',
|
||||
llmProvider: 'claude-code',
|
||||
dynamicBankId: false,
|
||||
bankIdPrefix: 'my-sandbox',
|
||||
};
|
||||
|
||||
const BASE_CONFIG: OpenClawConfig = {
|
||||
meta: { lastTouchedVersion: '2026.3.2' },
|
||||
gateway: { port: 18789, mode: 'local' },
|
||||
agents: {
|
||||
defaults: { model: { primary: 'openai/gpt-5' } },
|
||||
},
|
||||
plugins: {
|
||||
slots: { memory: 'memory-core' },
|
||||
entries: {
|
||||
'memory-core': { enabled: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('mergePluginConfig', () => {
|
||||
it('sets hindsight-openclaw as the memory slot', () => {
|
||||
const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG);
|
||||
expect(result.plugins?.slots?.memory).toBe('hindsight-openclaw');
|
||||
});
|
||||
|
||||
it('enables the hindsight-openclaw entry', () => {
|
||||
const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG);
|
||||
expect(result.plugins?.entries?.['hindsight-openclaw']?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('writes the full plugin config', () => {
|
||||
const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG);
|
||||
const config = result.plugins?.entries?.['hindsight-openclaw']?.config;
|
||||
expect(config?.hindsightApiUrl).toBe('https://api.hindsight.vectorize.io');
|
||||
expect(config?.hindsightApiToken).toBe('hsk_test123');
|
||||
expect(config?.llmProvider).toBe('claude-code');
|
||||
expect(config?.dynamicBankId).toBe(false);
|
||||
expect(config?.bankIdPrefix).toBe('my-sandbox');
|
||||
});
|
||||
|
||||
it('preserves existing top-level config fields', () => {
|
||||
const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG);
|
||||
expect(result.gateway).toEqual({ port: 18789, mode: 'local' });
|
||||
expect(result.agents).toBeDefined();
|
||||
});
|
||||
|
||||
it('preserves existing plugin entries', () => {
|
||||
const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG);
|
||||
expect(result.plugins?.entries?.['memory-core']?.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('merges into existing hindsight-openclaw entry without overwriting other fields', () => {
|
||||
const configWithExisting: OpenClawConfig = {
|
||||
...BASE_CONFIG,
|
||||
plugins: {
|
||||
...BASE_CONFIG.plugins,
|
||||
entries: {
|
||||
...BASE_CONFIG.plugins?.entries,
|
||||
'hindsight-openclaw': {
|
||||
enabled: true,
|
||||
config: { embedPackagePath: '/some/local/path' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = mergePluginConfig(configWithExisting, PLUGIN_CONFIG);
|
||||
const config = result.plugins?.entries?.['hindsight-openclaw']?.config;
|
||||
// New fields written
|
||||
expect(config?.hindsightApiUrl).toBe('https://api.hindsight.vectorize.io');
|
||||
// Existing custom field preserved
|
||||
expect(config?.embedPackagePath).toBe('/some/local/path');
|
||||
});
|
||||
|
||||
it('handles missing plugins section gracefully', () => {
|
||||
const minimal: OpenClawConfig = { gateway: { port: 18789 } };
|
||||
const result = mergePluginConfig(minimal, PLUGIN_CONFIG);
|
||||
expect(result.plugins?.slots?.memory).toBe('hindsight-openclaw');
|
||||
expect(result.plugins?.entries?.['hindsight-openclaw']?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('does not mutate the original config', () => {
|
||||
const original = JSON.parse(JSON.stringify(BASE_CONFIG)) as OpenClawConfig;
|
||||
mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG);
|
||||
expect(JSON.stringify(BASE_CONFIG)).toBe(JSON.stringify(original));
|
||||
});
|
||||
|
||||
it('records install metadata', () => {
|
||||
const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG);
|
||||
const install = result.plugins?.installs?.['hindsight-openclaw'] as Record<string, unknown>;
|
||||
expect(install?.source).toBe('npm');
|
||||
expect(install?.version).toBe('latest');
|
||||
expect(typeof install?.installedAt).toBe('string');
|
||||
});
|
||||
});
|
||||
106
hindsight-integrations/nemoclaw/src/openclaw-config.ts
Normal file
106
hindsight-integrations/nemoclaw/src/openclaw-config.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { readFile, writeFile, rename } from 'fs/promises';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
const CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
|
||||
|
||||
export interface HindsightPluginConfig {
|
||||
hindsightApiUrl: string;
|
||||
hindsightApiToken: string;
|
||||
llmProvider: string;
|
||||
dynamicBankId: boolean;
|
||||
bankIdPrefix: string;
|
||||
}
|
||||
|
||||
export interface OpenClawConfig {
|
||||
plugins?: {
|
||||
slots?: Record<string, string>;
|
||||
entries?: Record<string, { enabled: boolean; config?: Record<string, unknown> }>;
|
||||
installs?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export async function readOpenClawConfig(configPath = CONFIG_PATH): Promise<OpenClawConfig> {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(configPath, 'utf8');
|
||||
} catch (err: unknown) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === 'ENOENT') {
|
||||
throw new Error(
|
||||
`OpenClaw config not found at ${configPath}.\n` +
|
||||
`Run \`openclaw\` once to initialize it, then re-run setup.`
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return JSON.parse(raw) as OpenClawConfig;
|
||||
}
|
||||
|
||||
export function mergePluginConfig(
|
||||
config: OpenClawConfig,
|
||||
pluginConfig: HindsightPluginConfig
|
||||
): OpenClawConfig {
|
||||
const plugins = config.plugins ?? {};
|
||||
const entries = plugins.entries ?? {};
|
||||
const existing = entries['hindsight-openclaw'] ?? { enabled: true };
|
||||
|
||||
return {
|
||||
...config,
|
||||
plugins: {
|
||||
...plugins,
|
||||
slots: {
|
||||
...(plugins.slots ?? {}),
|
||||
memory: 'hindsight-openclaw',
|
||||
},
|
||||
entries: {
|
||||
...entries,
|
||||
'hindsight-openclaw': {
|
||||
...existing,
|
||||
enabled: true,
|
||||
config: {
|
||||
...(existing.config ?? {}),
|
||||
hindsightApiUrl: pluginConfig.hindsightApiUrl,
|
||||
hindsightApiToken: pluginConfig.hindsightApiToken,
|
||||
llmProvider: pluginConfig.llmProvider,
|
||||
dynamicBankId: pluginConfig.dynamicBankId,
|
||||
bankIdPrefix: pluginConfig.bankIdPrefix,
|
||||
},
|
||||
},
|
||||
},
|
||||
installs: {
|
||||
...(plugins.installs ?? {}),
|
||||
'hindsight-openclaw': {
|
||||
source: 'npm',
|
||||
version: 'latest',
|
||||
installedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeOpenClawConfig(
|
||||
config: OpenClawConfig,
|
||||
configPath = CONFIG_PATH
|
||||
): Promise<void> {
|
||||
const contents = JSON.stringify(config, null, 2) + '\n';
|
||||
const tmp = `${configPath}.${randomBytes(6).toString('hex')}.tmp`;
|
||||
await writeFile(tmp, contents, 'utf8');
|
||||
await rename(tmp, configPath);
|
||||
}
|
||||
|
||||
export async function applyPluginConfig(
|
||||
pluginConfig: HindsightPluginConfig,
|
||||
configPath = CONFIG_PATH
|
||||
): Promise<void> {
|
||||
const current = await readOpenClawConfig(configPath);
|
||||
const updated = mergePluginConfig(current, pluginConfig);
|
||||
await writeOpenClawConfig(updated, configPath);
|
||||
}
|
||||
|
||||
export { CONFIG_PATH };
|
||||
export { dirname };
|
||||
102
hindsight-integrations/nemoclaw/src/policy-reader.test.ts
Normal file
102
hindsight-integrations/nemoclaw/src/policy-reader.test.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { stripAnsi, extractPolicyYaml, parseSandboxPolicy } from './policy-reader.js';
|
||||
import { serializePolicy } from './policy-writer.js';
|
||||
|
||||
// Fixture: actual output of `openshell sandbox get my-assistant`
|
||||
// (ANSI codes represented as escape sequences)
|
||||
const FIXTURE_RAW = `\x1b[1m\x1b[36mSandbox:\x1b[39m\x1b[0m
|
||||
|
||||
\x1b[2mId:\x1b[0m 61c993f1-010f-4eca-a1ac-d6ddec9d604a
|
||||
\x1b[2mName:\x1b[0m my-assistant
|
||||
\x1b[2mNamespace:\x1b[0m openshell
|
||||
\x1b[2mPhase:\x1b[0m Ready
|
||||
|
||||
\x1b[1m\x1b[36mPolicy:\x1b[39m\x1b[0m
|
||||
|
||||
\x1b[2mversion\x1b[0m\x1b[2m:\x1b[0m 1
|
||||
\x1b[2mfilesystem_policy\x1b[0m\x1b[2m:\x1b[0m
|
||||
\x1b[2minclude_workdir\x1b[0m\x1b[2m:\x1b[0m true
|
||||
\x1b[2mread_only\x1b[0m\x1b[2m:\x1b[0m
|
||||
\x1b[2m- \x1b[0m/usr
|
||||
\x1b[2m- \x1b[0m/lib
|
||||
\x1b[2mread_write\x1b[0m\x1b[2m:\x1b[0m
|
||||
\x1b[2m- \x1b[0m/sandbox
|
||||
\x1b[2m- \x1b[0m/tmp
|
||||
\x1b[2mnetwork_policies\x1b[0m\x1b[2m:\x1b[0m
|
||||
\x1b[2mclaude_code\x1b[0m\x1b[2m:\x1b[0m
|
||||
\x1b[2mname\x1b[0m\x1b[2m:\x1b[0m claude_code
|
||||
\x1b[2mendpoints\x1b[0m\x1b[2m:\x1b[0m
|
||||
\x1b[2m- \x1b[0mhost: api.anthropic.com
|
||||
\x1b[2mport\x1b[0m\x1b[2m:\x1b[0m 443
|
||||
\x1b[2mrules\x1b[0m\x1b[2m:\x1b[0m
|
||||
\x1b[2m- \x1b[0mallow:
|
||||
\x1b[2mmethod\x1b[0m\x1b[2m:\x1b[0m '*'
|
||||
\x1b[2mpath\x1b[0m\x1b[2m:\x1b[0m /**
|
||||
\x1b[2mbinaries\x1b[0m\x1b[2m:\x1b[0m
|
||||
\x1b[2m- \x1b[0mpath: /usr/local/bin/claude
|
||||
`;
|
||||
|
||||
describe('stripAnsi', () => {
|
||||
it('removes ANSI escape codes', () => {
|
||||
expect(stripAnsi('\x1b[1m\x1b[36mHello\x1b[39m\x1b[0m')).toBe('Hello');
|
||||
});
|
||||
|
||||
it('leaves plain strings unchanged', () => {
|
||||
expect(stripAnsi('version: 1')).toBe('version: 1');
|
||||
});
|
||||
|
||||
it('handles strings with no ANSI codes', () => {
|
||||
expect(stripAnsi(' - /usr')).toBe(' - /usr');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractPolicyYaml', () => {
|
||||
it('extracts the Policy: section and dedents by 2 spaces', () => {
|
||||
const result = extractPolicyYaml(FIXTURE_RAW);
|
||||
expect(result).toContain('version: 1');
|
||||
expect(result).toContain('filesystem_policy:');
|
||||
expect(result).toContain('network_policies:');
|
||||
});
|
||||
|
||||
it('does not include the Sandbox: section', () => {
|
||||
const result = extractPolicyYaml(FIXTURE_RAW);
|
||||
expect(result).not.toContain('Sandbox:');
|
||||
expect(result).not.toContain('my-assistant');
|
||||
});
|
||||
|
||||
it('throws if Policy: section is missing', () => {
|
||||
expect(() => extractPolicyYaml('no policy here')).toThrow('Could not find "Policy:"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSandboxPolicy', () => {
|
||||
it('parses version field', () => {
|
||||
const policy = parseSandboxPolicy(FIXTURE_RAW);
|
||||
expect(policy.version).toBe(1);
|
||||
});
|
||||
|
||||
it('parses filesystem_policy', () => {
|
||||
const policy = parseSandboxPolicy(FIXTURE_RAW);
|
||||
expect(policy.filesystem_policy?.include_workdir).toBe(true);
|
||||
expect(policy.filesystem_policy?.read_only).toContain('/usr');
|
||||
});
|
||||
|
||||
it('parses network_policies', () => {
|
||||
const policy = parseSandboxPolicy(FIXTURE_RAW);
|
||||
expect(policy.network_policies).toBeDefined();
|
||||
expect(policy.network_policies?.claude_code).toBeDefined();
|
||||
expect(policy.network_policies?.claude_code?.name).toBe('claude_code');
|
||||
});
|
||||
|
||||
it('is idempotent — parse → serialize → parse yields same structure', () => {
|
||||
const policy1 = parseSandboxPolicy(FIXTURE_RAW);
|
||||
const yamlStr = serializePolicy(policy1);
|
||||
// Re-wrap in a Policy: header to match the expected format
|
||||
const wrapped = 'Policy:\n' + yamlStr.split('\n').map((l: string) => ` ${l}`).join('\n');
|
||||
const policy2 = parseSandboxPolicy(wrapped);
|
||||
expect(policy2.version).toBe(policy1.version);
|
||||
expect(Object.keys(policy2.network_policies ?? {})).toEqual(
|
||||
Object.keys(policy1.network_policies ?? {})
|
||||
);
|
||||
});
|
||||
});
|
||||
88
hindsight-integrations/nemoclaw/src/policy-reader.ts
Normal file
88
hindsight-integrations/nemoclaw/src/policy-reader.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import yaml from 'js-yaml';
|
||||
import type { SandboxPolicy } from './types.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/** Strip ANSI escape codes from a string */
|
||||
export function stripAnsi(str: string): string {
|
||||
return str.replace(/\x1B\[[0-9;]*m/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and dedent the policy section from `openshell sandbox get` output.
|
||||
* The output looks like:
|
||||
*
|
||||
* Sandbox:
|
||||
* Id: ...
|
||||
* Name: ...
|
||||
*
|
||||
* Policy:
|
||||
* version: 1
|
||||
* filesystem_policy:
|
||||
* ...
|
||||
*
|
||||
* We need to extract everything after `Policy:` and dedent by 2 spaces.
|
||||
*/
|
||||
export function extractPolicyYaml(raw: string): string {
|
||||
const stripped = stripAnsi(raw);
|
||||
const lines = stripped.split('\n');
|
||||
|
||||
const policyHeaderIdx = lines.findIndex(l => l.trimEnd() === 'Policy:');
|
||||
if (policyHeaderIdx === -1) {
|
||||
throw new Error('Could not find "Policy:" section in `openshell sandbox get` output');
|
||||
}
|
||||
|
||||
const policyLines = lines.slice(policyHeaderIdx + 1);
|
||||
|
||||
// Dedent by 2 spaces (the policy block is indented under `Policy:`)
|
||||
const dedented = policyLines.map(l => {
|
||||
if (l.startsWith(' ')) return l.slice(2);
|
||||
return l;
|
||||
});
|
||||
|
||||
// Drop trailing empty lines
|
||||
while (dedented.length > 0 && dedented[dedented.length - 1].trim() === '') {
|
||||
dedented.pop();
|
||||
}
|
||||
|
||||
return dedented.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `openshell sandbox get <name>` output into a SandboxPolicy object.
|
||||
* Throws a descriptive error if parsing fails.
|
||||
*/
|
||||
export function parseSandboxPolicy(rawOutput: string): SandboxPolicy {
|
||||
const policyYaml = extractPolicyYaml(rawOutput);
|
||||
|
||||
try {
|
||||
const parsed = yaml.load(policyYaml);
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
throw new Error('Parsed policy is not an object');
|
||||
}
|
||||
return parsed as SandboxPolicy;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to parse sandbox policy YAML.\n` +
|
||||
`This may mean the openshell output format has changed.\n` +
|
||||
`Apply the Hindsight policy manually using the instructions in NEMOCLAW.md.\n` +
|
||||
`Parse error: ${err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Run `openshell sandbox get <sandbox>` and return parsed policy */
|
||||
export async function readSandboxPolicy(sandboxName: string): Promise<SandboxPolicy> {
|
||||
let stdout: string;
|
||||
try {
|
||||
const result = await execFileAsync('openshell', ['sandbox', 'get', sandboxName]);
|
||||
stdout = result.stdout;
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Failed to run \`openshell sandbox get ${sandboxName}\`: ${msg}`);
|
||||
}
|
||||
|
||||
return parseSandboxPolicy(stdout);
|
||||
}
|
||||
107
hindsight-integrations/nemoclaw/src/policy-writer.test.ts
Normal file
107
hindsight-integrations/nemoclaw/src/policy-writer.test.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { hasHindsightPolicy, mergeHindsightPolicy, serializePolicy } from './policy-writer.js';
|
||||
import { parseSandboxPolicy } from './policy-reader.js';
|
||||
import type { SandboxPolicy } from './types.js';
|
||||
import { HINDSIGHT_HOST, OPENCLAW_BINARY } from './types.js';
|
||||
|
||||
const BASE_POLICY: SandboxPolicy = {
|
||||
version: 1,
|
||||
filesystem_policy: {
|
||||
include_workdir: true,
|
||||
read_only: ['/usr', '/lib'],
|
||||
read_write: ['/sandbox', '/tmp'],
|
||||
},
|
||||
network_policies: {
|
||||
claude_code: {
|
||||
name: 'claude_code',
|
||||
endpoints: [
|
||||
{
|
||||
host: 'api.anthropic.com',
|
||||
port: 443,
|
||||
rules: [{ allow: { method: '*', path: '/**' } }],
|
||||
},
|
||||
],
|
||||
binaries: [{ path: '/usr/local/bin/claude' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('hasHindsightPolicy', () => {
|
||||
it('returns false when no hindsight policy exists', () => {
|
||||
expect(hasHindsightPolicy(BASE_POLICY)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when network_policies is undefined', () => {
|
||||
expect(hasHindsightPolicy({ version: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when hindsight policy is present', () => {
|
||||
const withHindsight = mergeHindsightPolicy(BASE_POLICY);
|
||||
expect(hasHindsightPolicy(withHindsight)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeHindsightPolicy', () => {
|
||||
it('adds the hindsight network policy block', () => {
|
||||
const result = mergeHindsightPolicy(BASE_POLICY);
|
||||
expect(result.network_policies?.hindsight).toBeDefined();
|
||||
expect(result.network_policies?.hindsight?.endpoints[0].host).toBe(HINDSIGHT_HOST);
|
||||
});
|
||||
|
||||
it('preserves all existing network policies', () => {
|
||||
const result = mergeHindsightPolicy(BASE_POLICY);
|
||||
expect(result.network_policies?.claude_code).toBeDefined();
|
||||
expect(result.network_policies?.claude_code?.name).toBe('claude_code');
|
||||
});
|
||||
|
||||
it('sets the correct binary path', () => {
|
||||
const result = mergeHindsightPolicy(BASE_POLICY);
|
||||
const binaries = result.network_policies?.hindsight?.binaries ?? [];
|
||||
expect(binaries.some(b => b.path === OPENCLAW_BINARY)).toBe(true);
|
||||
});
|
||||
|
||||
it('includes GET, POST, and PUT rules', () => {
|
||||
const result = mergeHindsightPolicy(BASE_POLICY);
|
||||
const rules = result.network_policies?.hindsight?.endpoints[0].rules ?? [];
|
||||
const methods = rules.map(r => r.allow.method);
|
||||
expect(methods).toContain('GET');
|
||||
expect(methods).toContain('POST');
|
||||
expect(methods).toContain('PUT');
|
||||
});
|
||||
|
||||
it('is idempotent — merging twice yields the same result', () => {
|
||||
const once = mergeHindsightPolicy(BASE_POLICY);
|
||||
const twice = mergeHindsightPolicy(once);
|
||||
expect(JSON.stringify(twice.network_policies?.hindsight)).toBe(
|
||||
JSON.stringify(once.network_policies?.hindsight)
|
||||
);
|
||||
});
|
||||
|
||||
it('does not mutate the original policy', () => {
|
||||
const original = JSON.parse(JSON.stringify(BASE_POLICY)) as SandboxPolicy;
|
||||
mergeHindsightPolicy(BASE_POLICY);
|
||||
expect(BASE_POLICY.network_policies?.hindsight).toBeUndefined();
|
||||
expect(JSON.stringify(BASE_POLICY)).toBe(JSON.stringify(original));
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializePolicy', () => {
|
||||
it('produces valid YAML that round-trips through parseSandboxPolicy', () => {
|
||||
const merged = mergeHindsightPolicy(BASE_POLICY);
|
||||
const yamlStr = serializePolicy(merged);
|
||||
// Wrap in Policy: header as parseSandboxPolicy expects
|
||||
const wrapped = 'Policy:\n' + yamlStr.split('\n').map(l => ` ${l}`).join('\n');
|
||||
const reparsed = parseSandboxPolicy(wrapped);
|
||||
expect(reparsed.version).toBe(merged.version);
|
||||
expect(reparsed.network_policies?.hindsight?.endpoints[0].host).toBe(HINDSIGHT_HOST);
|
||||
expect(reparsed.network_policies?.claude_code).toBeDefined();
|
||||
});
|
||||
|
||||
it('includes all network policies in output', () => {
|
||||
const merged = mergeHindsightPolicy(BASE_POLICY);
|
||||
const yaml = serializePolicy(merged);
|
||||
expect(yaml).toContain('claude_code:');
|
||||
expect(yaml).toContain('hindsight:');
|
||||
expect(yaml).toContain(HINDSIGHT_HOST);
|
||||
});
|
||||
});
|
||||
59
hindsight-integrations/nemoclaw/src/policy-writer.ts
Normal file
59
hindsight-integrations/nemoclaw/src/policy-writer.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import yaml from 'js-yaml';
|
||||
import type { SandboxPolicy } from './types.js';
|
||||
import { HINDSIGHT_POLICY_NAME, HINDSIGHT_HOST, OPENCLAW_BINARY } from './types.js';
|
||||
|
||||
const HINDSIGHT_NETWORK_POLICY = {
|
||||
name: HINDSIGHT_POLICY_NAME,
|
||||
endpoints: [
|
||||
{
|
||||
host: HINDSIGHT_HOST,
|
||||
port: 443,
|
||||
protocol: 'rest',
|
||||
tls: 'terminate',
|
||||
enforcement: 'enforce',
|
||||
rules: [
|
||||
{ allow: { method: 'GET', path: '/**' } },
|
||||
{ allow: { method: 'POST', path: '/**' } },
|
||||
{ allow: { method: 'PUT', path: '/**' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
binaries: [{ path: OPENCLAW_BINARY }],
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the policy already has a correct Hindsight network policy entry.
|
||||
*/
|
||||
export function hasHindsightPolicy(policy: SandboxPolicy): boolean {
|
||||
const np = policy.network_policies?.[HINDSIGHT_POLICY_NAME];
|
||||
if (!np) return false;
|
||||
return np.endpoints?.some(e => e.host === HINDSIGHT_HOST) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the Hindsight network policy block into a SandboxPolicy.
|
||||
* Idempotent — if the block already exists and is correct, returns policy unchanged.
|
||||
*/
|
||||
export function mergeHindsightPolicy(policy: SandboxPolicy): SandboxPolicy {
|
||||
const updated: SandboxPolicy = {
|
||||
...policy,
|
||||
network_policies: {
|
||||
...(policy.network_policies ?? {}),
|
||||
[HINDSIGHT_POLICY_NAME]: HINDSIGHT_NETWORK_POLICY,
|
||||
},
|
||||
};
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a SandboxPolicy to a YAML string suitable for `openshell policy set`.
|
||||
*/
|
||||
export function serializePolicy(policy: SandboxPolicy): string {
|
||||
return yaml.dump(policy, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
noRefs: true,
|
||||
quotingType: '"',
|
||||
forceQuotes: false,
|
||||
});
|
||||
}
|
||||
173
hindsight-integrations/nemoclaw/src/setup.test.ts
Normal file
173
hindsight-integrations/nemoclaw/src/setup.test.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { CliArgs } from './types.js';
|
||||
|
||||
// Mock all external I/O before importing setup
|
||||
vi.mock('child_process', () => ({
|
||||
execFile: vi.fn(),
|
||||
}));
|
||||
vi.mock('./policy-reader.js', () => ({
|
||||
readSandboxPolicy: vi.fn(),
|
||||
}));
|
||||
vi.mock('./policy-writer.js', () => ({
|
||||
hasHindsightPolicy: vi.fn(),
|
||||
mergeHindsightPolicy: vi.fn(),
|
||||
serializePolicy: vi.fn(),
|
||||
}));
|
||||
vi.mock('./openclaw-config.js', () => ({
|
||||
applyPluginConfig: vi.fn(),
|
||||
}));
|
||||
vi.mock('fs/promises', () => ({
|
||||
writeFile: vi.fn(),
|
||||
rm: vi.fn(),
|
||||
}));
|
||||
|
||||
const BASE_ARGS: CliArgs = {
|
||||
sandbox: 'my-assistant',
|
||||
apiUrl: 'https://api.hindsight.vectorize.io',
|
||||
apiToken: 'hsk_test123',
|
||||
bankPrefix: 'my-sandbox',
|
||||
skipPolicy: false,
|
||||
skipPluginInstall: false,
|
||||
dryRun: false,
|
||||
};
|
||||
|
||||
describe('runSetup', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const { execFile } = await import('child_process');
|
||||
const execFileMock = vi.mocked(execFile);
|
||||
|
||||
// Default: all shell commands succeed
|
||||
execFileMock.mockImplementation((_cmd, _args, callback?: unknown) => {
|
||||
if (typeof callback === 'function') {
|
||||
(callback as (err: null, result: { stdout: string; stderr: string }) => void)(
|
||||
null, { stdout: '', stderr: '' }
|
||||
);
|
||||
}
|
||||
return {} as ReturnType<typeof execFile>;
|
||||
});
|
||||
|
||||
const { readSandboxPolicy } = await import('./policy-reader.js');
|
||||
vi.mocked(readSandboxPolicy).mockResolvedValue({
|
||||
version: 1,
|
||||
network_policies: { claude_code: { name: 'claude_code', endpoints: [] } },
|
||||
});
|
||||
|
||||
const { hasHindsightPolicy, mergeHindsightPolicy, serializePolicy } = await import('./policy-writer.js');
|
||||
vi.mocked(hasHindsightPolicy).mockReturnValue(false);
|
||||
vi.mocked(mergeHindsightPolicy).mockImplementation(p => ({ ...p, network_policies: { ...p.network_policies, hindsight: { name: 'hindsight', endpoints: [] } } }));
|
||||
vi.mocked(serializePolicy).mockReturnValue('version: 1\n');
|
||||
|
||||
const { applyPluginConfig } = await import('./openclaw-config.js');
|
||||
vi.mocked(applyPluginConfig).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('runs all steps in order for a clean install', async () => {
|
||||
const { execFile } = await import('child_process');
|
||||
const calls: string[] = [];
|
||||
|
||||
vi.mocked(execFile).mockImplementation((cmd, args, callback?: unknown) => {
|
||||
calls.push(`${cmd} ${(args as string[]).join(' ')}`);
|
||||
if (typeof callback === 'function') {
|
||||
(callback as (err: null, result: { stdout: string; stderr: string }) => void)(
|
||||
null, { stdout: '', stderr: '' }
|
||||
);
|
||||
}
|
||||
return {} as ReturnType<typeof execFile>;
|
||||
});
|
||||
|
||||
const { runSetup } = await import('./setup.js');
|
||||
await runSetup(BASE_ARGS);
|
||||
|
||||
expect(calls.some(c => c.includes('which openshell'))).toBe(true);
|
||||
expect(calls.some(c => c.includes('which openclaw'))).toBe(true);
|
||||
expect(calls.some(c => c.includes('openclaw plugins install @vectorize-io/hindsight-openclaw'))).toBe(true);
|
||||
expect(calls.some(c => c.includes('openshell policy set my-assistant'))).toBe(true);
|
||||
expect(calls.some(c => c.includes('openclaw gateway restart'))).toBe(true);
|
||||
});
|
||||
|
||||
it('skips plugin install when --skip-plugin-install is set', async () => {
|
||||
const { execFile } = await import('child_process');
|
||||
const calls: string[] = [];
|
||||
vi.mocked(execFile).mockImplementation((cmd, args, callback?: unknown) => {
|
||||
calls.push(`${cmd} ${(args as string[]).join(' ')}`);
|
||||
if (typeof callback === 'function') {
|
||||
(callback as (err: null, result: { stdout: string; stderr: string }) => void)(
|
||||
null, { stdout: '', stderr: '' }
|
||||
);
|
||||
}
|
||||
return {} as ReturnType<typeof execFile>;
|
||||
});
|
||||
|
||||
const { runSetup } = await import('./setup.js');
|
||||
await runSetup({ ...BASE_ARGS, skipPluginInstall: true });
|
||||
expect(calls.some(c => c.includes('plugins install'))).toBe(false);
|
||||
});
|
||||
|
||||
it('skips policy update when --skip-policy is set', async () => {
|
||||
const { runSetup } = await import('./setup.js');
|
||||
const { readSandboxPolicy } = await import('./policy-reader.js');
|
||||
await runSetup({ ...BASE_ARGS, skipPolicy: true });
|
||||
expect(vi.mocked(readSandboxPolicy)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips policy set when Hindsight policy already exists', async () => {
|
||||
const { hasHindsightPolicy } = await import('./policy-writer.js');
|
||||
vi.mocked(hasHindsightPolicy).mockReturnValue(true);
|
||||
|
||||
const { execFile } = await import('child_process');
|
||||
const calls: string[] = [];
|
||||
vi.mocked(execFile).mockImplementation((cmd, args, callback?: unknown) => {
|
||||
calls.push(`${cmd} ${(args as string[]).join(' ')}`);
|
||||
if (typeof callback === 'function') {
|
||||
(callback as (err: null, result: { stdout: string; stderr: string }) => void)(
|
||||
null, { stdout: '', stderr: '' }
|
||||
);
|
||||
}
|
||||
return {} as ReturnType<typeof execFile>;
|
||||
});
|
||||
|
||||
const { runSetup } = await import('./setup.js');
|
||||
await runSetup(BASE_ARGS);
|
||||
expect(calls.some(c => c.includes('openshell policy set'))).toBe(false);
|
||||
});
|
||||
|
||||
it('does not execute any shell commands in dry-run mode', async () => {
|
||||
const { execFile } = await import('child_process');
|
||||
const { applyPluginConfig } = await import('./openclaw-config.js');
|
||||
const { writeFile } = await import('fs/promises');
|
||||
|
||||
const { runSetup } = await import('./setup.js');
|
||||
await runSetup({ ...BASE_ARGS, dryRun: true });
|
||||
|
||||
// which checks still run (preflight), but no actual commands
|
||||
const execCalls = vi.mocked(execFile).mock.calls.map(c => `${c[0]} ${(c[1] as string[]).join(' ')}`);
|
||||
expect(execCalls.some(c => c.includes('plugins install'))).toBe(false);
|
||||
expect(execCalls.some(c => c.includes('policy set'))).toBe(false);
|
||||
expect(execCalls.some(c => c.includes('gateway restart'))).toBe(false);
|
||||
expect(vi.mocked(applyPluginConfig)).not.toHaveBeenCalled();
|
||||
expect(vi.mocked(writeFile)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails early if openshell is not on PATH', async () => {
|
||||
const { execFile } = await import('child_process');
|
||||
vi.mocked(execFile).mockImplementation((cmd, args, callback?: unknown) => {
|
||||
if (cmd === 'which' && (args as string[])[0] === 'openshell') {
|
||||
if (typeof callback === 'function') {
|
||||
(callback as (err: Error) => void)(new Error('not found'));
|
||||
}
|
||||
} else {
|
||||
if (typeof callback === 'function') {
|
||||
(callback as (err: null, result: { stdout: string; stderr: string }) => void)(
|
||||
null, { stdout: '', stderr: '' }
|
||||
);
|
||||
}
|
||||
}
|
||||
return {} as ReturnType<typeof execFile>;
|
||||
});
|
||||
|
||||
const { runSetup } = await import('./setup.js');
|
||||
await expect(runSetup(BASE_ARGS)).rejects.toThrow('openshell');
|
||||
});
|
||||
});
|
||||
148
hindsight-integrations/nemoclaw/src/setup.ts
Normal file
148
hindsight-integrations/nemoclaw/src/setup.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
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?"`);
|
||||
}
|
||||
63
hindsight-integrations/nemoclaw/src/types.ts
Normal file
63
hindsight-integrations/nemoclaw/src/types.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
export interface CliArgs {
|
||||
sandbox: string;
|
||||
apiUrl: string;
|
||||
apiToken: string;
|
||||
bankPrefix: string;
|
||||
skipPolicy: boolean;
|
||||
skipPluginInstall: boolean;
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
export interface PolicyEndpointRule {
|
||||
allow: {
|
||||
method: string;
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PolicyEndpoint {
|
||||
host: string;
|
||||
port: number;
|
||||
protocol?: string;
|
||||
tls?: string;
|
||||
enforcement?: string;
|
||||
access?: string;
|
||||
rules?: PolicyEndpointRule[];
|
||||
}
|
||||
|
||||
export interface PolicyBinary {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface NetworkPolicy {
|
||||
name: string;
|
||||
endpoints: PolicyEndpoint[];
|
||||
binaries?: PolicyBinary[];
|
||||
}
|
||||
|
||||
export interface FilesystemPolicy {
|
||||
include_workdir?: boolean;
|
||||
read_only?: string[];
|
||||
read_write?: string[];
|
||||
}
|
||||
|
||||
export interface Landlock {
|
||||
compatibility?: string;
|
||||
}
|
||||
|
||||
export interface ProcessPolicy {
|
||||
run_as_user?: string;
|
||||
run_as_group?: string;
|
||||
}
|
||||
|
||||
export interface SandboxPolicy {
|
||||
version?: number;
|
||||
filesystem_policy?: FilesystemPolicy;
|
||||
landlock?: Landlock;
|
||||
process?: ProcessPolicy;
|
||||
network_policies?: Record<string, NetworkPolicy>;
|
||||
}
|
||||
|
||||
export const HINDSIGHT_POLICY_NAME = 'hindsight';
|
||||
export const HINDSIGHT_HOST = 'api.hindsight.vectorize.io';
|
||||
export const OPENCLAW_BINARY = '/usr/local/bin/openclaw';
|
||||
18
hindsight-integrations/nemoclaw/tsconfig.json
Normal file
18
hindsight-integrations/nemoclaw/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
|
||||
}
|
||||
9
hindsight-integrations/nemoclaw/vitest.config.ts
Normal file
9
hindsight-integrations/nemoclaw/vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue