From 0c64045a44af75af0c8f426f93f97e2bf9b7d7f2 Mon Sep 17 00:00:00 2001 From: AICtrl Bot Date: Tue, 3 Mar 2026 10:08:21 +0000 Subject: [PATCH] feat: add Hush safety skill and proxy support for OpenClaw --- README.md | 26 +++ .../.openclaw/skills/hush/SKILL.md | 27 +++ .../.openclaw/skills/hush/index.ts | 11 ++ package-lock.json | 11 +- package.json | 6 + scripts/e2e-openclaw.ts | 155 ++++++++++++++++++ src/plugins/openclaw-hush.ts | 76 +++++++++ tests/openclaw-plugin.test.ts | 85 ++++++++++ 8 files changed, 395 insertions(+), 2 deletions(-) create mode 100644 examples/team-config/.openclaw/skills/hush/SKILL.md create mode 100644 examples/team-config/.openclaw/skills/hush/index.ts create mode 100644 scripts/e2e-openclaw.ts create mode 100644 src/plugins/openclaw-hush.ts create mode 100644 tests/openclaw-plugin.test.ts diff --git a/README.md b/README.md index 5d79599..e072ee3 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ your-project/ ├── .claude/settings.json # Claude Code → hush ├── .codex/config.toml # Codex → hush ├── .gemini/.env # Gemini CLI → hush +├── .openclaw/ # OpenClaw skill workspace └── opencode.json # OpenCode → hush ``` @@ -220,6 +221,30 @@ Tool reads config.txt → [Plugin: allowed] → proxy redacts PII → (not a sensitive filename) ``` +## OpenClaw Skill + +Hush provides a **safety skill** for OpenClaw that blocks dangerous file reads and redacts PII from tool outputs *locally* before the model ever sees them. + +### Setup + +Copy the skill directory into your OpenClaw workspace: + +```bash +mkdir -p ~/.openclaw/workspace/skills/hush +cp examples/team-config/.openclaw/skills/hush/* ~/.openclaw/workspace/skills/hush/ +``` + +### What it protects + +1. **Pre-execution Blocking**: Stops tools like `read` or `bash` if they target sensitive files (e.g., `.env`, `*.pem`, `id_rsa`). +2. **Post-execution Redaction**: Automatically scans `stdout`/`stderr` and file content for PII (emails, IPs, keys) and swaps them for tokens before returning the result to the model. + +### npm import + +```typescript +import { HushSkill } from '@aictrl/hush/openclaw-skill' +``` + ## How it Works 1. **Intercept** — Hush sits on your machine between your AI tool and the LLM provider. @@ -235,6 +260,7 @@ Tool reads config.txt → [Plugin: allowed] → proxy redacts PII → | Claude Code | `~/.claude/settings.json` | `/v1/messages` → Anthropic | | Codex | `~/.codex/config.toml` | `/v1/chat/completions` → OpenAI | | OpenCode | `opencode.json` | `/api/paas/v4/**` → ZhipuAI | +| OpenClaw | `~/.openclaw/openclaw.json` | `/*` (Proxy) + Skill | | Gemini CLI | `.gemini/.env` | `/v1beta/models/**` → Google | | Any tool | Point base URL at hush | `/*` catch-all with auto-detect | diff --git a/examples/team-config/.openclaw/skills/hush/SKILL.md b/examples/team-config/.openclaw/skills/hush/SKILL.md new file mode 100644 index 0000000..14a84b2 --- /dev/null +++ b/examples/team-config/.openclaw/skills/hush/SKILL.md @@ -0,0 +1,27 @@ +# Hush PII Guard 🛡️ + +**Hush PII Guard** is a safety skill for OpenClaw that prevents sensitive data from being sent to AI models or leaking through tool outputs. + +## What it blocks +| Tool | Blocked when | +|------|-------------| +| `read` | File path matches `.env*`, `*credentials*`, `*secret*`, `*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `*.asc`, `id_rsa*`, `.netrc`, `.pgpass` | +| `bash` | Commands like `cat`, `head`, `tail`, `less`, `more`, `bat` target a sensitive file | + +## What it redacts +The skill automatically scans the output of every tool (Bash stdout/stderr, file reads, etc.) for: +- 📧 Emails +- 🌐 IP Addresses +- 🔑 API Keys & Secrets +- 💳 Credit Card Numbers +- 📞 Phone Numbers + +Sensitive data is replaced with deterministic tokens like `[USER_EMAIL_f22c5a]`. + +## Setup +1. Copy this directory to `~/.openclaw/workspace/skills/hush/`. +2. Ensure `hush` is installed globally: `npm install -g @aictrl/hush`. +3. OpenClaw will automatically discover and load the skill from your workspace. + +## Defense-in-depth +For maximum protection, use this skill alongside the **Hush Proxy**. The skill protects your local files, while the proxy redacts PII from API requests before they leave your machine. diff --git a/examples/team-config/.openclaw/skills/hush/index.ts b/examples/team-config/.openclaw/skills/hush/index.ts new file mode 100644 index 0000000..b9a158f --- /dev/null +++ b/examples/team-config/.openclaw/skills/hush/index.ts @@ -0,0 +1,11 @@ +/** + * OpenClaw Skill: Hush PII Guard + * + * This file is a wrapper that imports the implementation from the main package. + * In a real-world deployment, you would copy the contents of `src/plugins/openclaw-hush.ts` + * into this file. + */ + +import { HushSkill } from '@aictrl/hush/openclaw-skill'; + +export default HushSkill; diff --git a/package-lock.json b/package-lock.json index 66b2e35..a57f7bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aictrl/hush", - "version": "0.1.6", + "version": "0.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aictrl/hush", - "version": "0.1.6", + "version": "0.1.8", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", @@ -15,6 +15,7 @@ "cors": "^2.8.6", "dotenv": "^17.3.1", "express": "^5.2.1", + "openclaw": "^0.0.1", "pino": "^10.3.1", "pino-pretty": "^13.1.3" }, @@ -3042,6 +3043,12 @@ "wrappy": "1" } }, + "node_modules/openclaw": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-0.0.1.tgz", + "integrity": "sha512-RjBpKUdV8BeVBDWd3vJi4Okl7AwDwC/yKsP6tf89CQIH+B+M6J0SsxkyJqd5Kc/c4bZkJ7mWYSd4eYo4Jzc7mA==", + "license": "UNLICENSED" + }, "node_modules/optimist": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", diff --git a/package.json b/package.json index 5fe663f..6f86a70 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,10 @@ "./opencode-plugin": { "import": "./dist/plugins/opencode-hush.js", "types": "./dist/plugins/opencode-hush.d.ts" + }, + "./openclaw-skill": { + "import": "./dist/plugins/openclaw-hush.js", + "types": "./dist/plugins/openclaw-hush.d.ts" } }, "bin": { @@ -24,6 +28,7 @@ "dev": "tsx src/cli.ts", "test": "vitest run --coverage", "test:e2e": "./scripts/e2e-opencode.sh", + "test:openclaw": "tsx scripts/e2e-openclaw.ts", "test:watch": "vitest", "lint": "eslint src/**/*.ts", "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"" @@ -49,6 +54,7 @@ "cors": "^2.8.6", "dotenv": "^17.3.1", "express": "^5.2.1", + "openclaw": "^0.0.1", "pino": "^10.3.1", "pino-pretty": "^13.1.3" }, diff --git a/scripts/e2e-openclaw.ts b/scripts/e2e-openclaw.ts new file mode 100644 index 0000000..a932a76 --- /dev/null +++ b/scripts/e2e-openclaw.ts @@ -0,0 +1,155 @@ +/** + * Simulated E2E test for OpenClaw integration. + * + * This test simulates the two layers of protection: + * 1. The Skill (Local): Intercepts tool calls and redacts their outputs. + * 2. The Proxy (Cloud): Intercepts API requests from OpenClaw and redacts PII. + */ + +import http from 'node:http'; +import fs from 'node:fs'; +import { HushSkill } from '../src/plugins/openclaw-hush.js'; +import { Redactor } from '../src/middleware/redactor.js'; +import { TokenVault } from '../src/vault/token-vault.js'; + +// Configuration +const MOCK_PORT = 4991; +const GATEWAY_PORT = 4992; +const CAPTURE_FILE = '/tmp/hush-openclaw-captured.json'; + +// --- Step 1: Mock Upstream (ZhipuAI/OpenAI) --- +const mockUpstream = http.createServer((req, res) => { + if (req.method === 'POST') { + let body = ''; + req.on('data', chunk => { body += chunk; }); + req.on('end', () => { + fs.writeFileSync(CAPTURE_FILE, body); + const parsed = JSON.parse(body); + const lastMessage = parsed.messages?.[parsed.messages.length - 1]?.content || ''; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + id: 'chatcmpl-e2e-mock-001', + choices: [{ + message: { role: 'assistant', content: `Echo: ${lastMessage}` } + }] + })); + }); + } else if (req.url === '/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok' })); + } +}); + +// --- Step 2: Hush Gateway (Proxy) --- +const redactor = new Redactor(); +const vault = new TokenVault(); + +const gateway = http.createServer(async (req, res) => { + if (req.method === 'POST') { + let body = ''; + req.on('data', chunk => { body += chunk; }); + req.on('end', async () => { + const bodyParsed = JSON.parse(body); + const { content: redactedBody, tokens, hasRedacted } = redactor.redact(bodyParsed); + if (hasRedacted) { + vault.saveTokens(tokens); + } + + const upstreamRes = await fetch(`http://127.0.0.1:${MOCK_PORT}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(redactedBody) + }); + const data = await upstreamRes.json(); + const rehydrated = vault.rehydrate(data); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(rehydrated)); + }); + } else if (req.url === '/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok', vaultSize: vault.size })); + } +}); + +// --- Main Test Routine --- +async function runTest() { + console.log('--- OpenClaw Simulated E2E Test ---'); + + mockUpstream.listen(MOCK_PORT, '127.0.0.1'); + gateway.listen(GATEWAY_PORT, '127.0.0.1'); + + try { + const skill = await HushSkill(); + + // 1. Test SKILL Blocking (Pre-execution) + console.log('[1/4] Testing Skill Blocking...'); + const blockResult = await skill['before_tool_call']({ + toolName: 'read', + params: { filePath: '.env' } + }); + if (blockResult?.block === true) { + console.log(' PASS: Blocked sensitive file read'); + } else { + throw new Error('Should have blocked .env read'); + } + + // 2. Test SKILL Redaction (Post-execution) + console.log('[2/4] Testing Skill Output Redaction...'); + const event = { + toolName: 'bash', + params: {}, + result: { stdout: 'My secret email is bulat@aictrl.dev' } + }; + await skill['after_tool_call'](event); + if (event.result.stdout.includes('bulat@aictrl.dev')) { + throw new Error('Skill failed to redact output'); + } + console.log(' PASS: Redacted tool output before AI sees it'); + console.log(` Output: ${event.result.stdout}`); + + // 3. Test PROXY Redaction (Outgoing API) + console.log('[3/4] Testing Proxy API Redaction...'); + const apiRequest = { + model: 'glm-5', + messages: [{ role: 'user', content: `Contact another-email@example.com for help.` }] + }; + + const res = await fetch(`http://127.0.0.1:${GATEWAY_PORT}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(apiRequest) + }); + + const apiResponse = await res.json(); + const captured = JSON.parse(fs.readFileSync(CAPTURE_FILE, 'utf8')); + const messageToUpstream = captured.messages[0].content; + + if (messageToUpstream.includes('another-email@example.com')) { + throw new Error('Proxy failed to redact outgoing PII'); + } + console.log(' PASS: Redacted PII in cloud request'); + + // 4. Test PROXY Rehydration (Incoming Response) + console.log('[4/4] Testing Proxy Response Rehydration...'); + const assistantContent = apiResponse.choices[0].message.content; + + if (!assistantContent.includes('another-email@example.com')) { + throw new Error(`Proxy failed to rehydrate response.`); + } + console.log(' PASS: Rehydrated PII in response to OpenClaw'); + console.log(` Assistant: ${assistantContent}`); + + console.log('\n--- ALL E2E CHECKS PASSED ---'); + } catch (err) { + console.error('\n--- E2E TEST FAILED ---'); + console.error(err); + process.exit(1); + } finally { + mockUpstream.close(); + gateway.close(); + if (fs.existsSync(CAPTURE_FILE)) fs.unlinkSync(CAPTURE_FILE); + } +} + +runTest(); diff --git a/src/plugins/openclaw-hush.ts b/src/plugins/openclaw-hush.ts new file mode 100644 index 0000000..f2f6f2e --- /dev/null +++ b/src/plugins/openclaw-hush.ts @@ -0,0 +1,76 @@ +/** + * OpenClaw Skill: Hush PII Guard + * + * 1. Blocks reads of sensitive files (`.env`, `*.pem`, `credentials.*`, etc.) + * before the tool executes — the AI model never sees the content. + * 2. Redacts PII (emails, IPs, secrets) from tool outputs (stdout/stderr) + * before the AI model sees the result. + * + * Defense-in-depth: works alongside the Hush proxy which redacts PII from + * API requests. The skill protects your local machine; the proxy protects the cloud. + * + * Install: + * Copy this file to `~/.openclaw/workspace/skills/hush/index.ts` + * and create a `SKILL.md` in the same directory. + */ + +import { isSensitivePath, commandReadsSensitiveFile } from './sensitive-patterns.js'; +import { Redactor } from '../middleware/redactor.js'; + +const redactor = new Redactor(); + +export const HushSkill = async () => ({ + /** + * Pre-execution: Block dangerous file access. + */ + 'before_tool_call': async ( + event: { toolName: string; params: Record }, + ) => { + // Block read tool if targeting sensitive files + if (event.toolName === 'read' && isSensitivePath(event.params['filePath'] ?? '')) { + return { block: true, blockReason: '[hush] Blocked: sensitive file' }; + } + + // Block bash tool if command reads sensitive files (cat .env, etc.) + if (event.toolName === 'bash' && commandReadsSensitiveFile(event.params['command'] ?? '')) { + return { block: true, blockReason: '[hush] Blocked: command reads sensitive file' }; + } + }, + + /** + * Post-execution: Redact PII from tool outputs before OpenClaw sees them. + */ + 'after_tool_call': async ( + event: { + toolName: string; + params: Record; + result?: any; + }, + ) => { + if (!event.result || typeof event.result !== 'object') return; + + const output = event.result; + + // 1. Scan stdout/stderr (Bash tool) + if (output.stdout) { + const { content: redacted } = redactor.redact(output.stdout); + output.stdout = redacted as string; + } + if (output.stderr) { + const { content: redacted } = redactor.redact(output.stderr); + output.stderr = redacted as string; + } + + // 2. Scan file content (Read tool) + if (output.file && typeof output.file.content === 'string') { + const { content: redacted } = redactor.redact(output.file.content); + output.file.content = redacted as string; + } + + // 3. Scan generic content/output + if (typeof output.content === 'string') { + const { content: redacted } = redactor.redact(output.content); + output.content = redacted as string; + } + }, +}); diff --git a/tests/openclaw-plugin.test.ts b/tests/openclaw-plugin.test.ts new file mode 100644 index 0000000..9a9c77c --- /dev/null +++ b/tests/openclaw-plugin.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from 'vitest'; +import { HushSkill } from '../src/plugins/openclaw-hush.js'; + +describe('HushSkill (OpenClaw)', () => { + it('exports a factory that returns tool call hooks', async () => { + const skill = await HushSkill(); + expect(skill['before_tool_call']).toBeTypeOf('function'); + expect(skill['after_tool_call']).toBeTypeOf('function'); + }); + + describe('before_tool_call (Blocking)', () => { + it('returns block:true when read targets a sensitive file', async () => { + const skill = await HushSkill(); + const result = await skill['before_tool_call']({ + toolName: 'read', + params: { filePath: '.env' } + }); + expect(result).toEqual({ block: true, blockReason: '[hush] Blocked: sensitive file' }); + }); + + it('returns block:true when bash command reads a sensitive file', async () => { + const skill = await HushSkill(); + const result = await skill['before_tool_call']({ + toolName: 'bash', + params: { command: 'cat .env' } + }); + expect(result).toEqual({ block: true, blockReason: '[hush] Blocked: command reads sensitive file' }); + }); + + it('returns undefined for harmless read', async () => { + const skill = await HushSkill(); + const result = await skill['before_tool_call']({ + toolName: 'read', + params: { filePath: 'package.json' } + }); + expect(result).toBeUndefined(); + }); + }); + + describe('after_tool_call (Redaction)', () => { + it('redacts PII from bash stdout', async () => { + const skill = await HushSkill(); + const event = { + toolName: 'bash', + params: {}, + result: { stdout: 'My email is bulat@example.com' } + }; + + await skill['after_tool_call'](event); + + expect(event.result.stdout).not.toContain('bulat@example.com'); + expect(event.result.stdout).toContain('[USER_EMAIL_'); + }); + + it('redacts PII from read file content', async () => { + const skill = await HushSkill(); + const event = { + toolName: 'read', + params: {}, + result: { + file: { content: 'Server is at 127.0.0.1 and use key sk-ant-123456789012345678901234567890123456' } + } + }; + + await skill['after_tool_call'](event); + + expect(event.result.file.content).not.toContain('127.0.0.1'); + expect(event.result.file.content).not.toContain('sk-ant-'); + }); + + it('redacts PII from generic content field', async () => { + const skill = await HushSkill(); + const event = { + toolName: 'web_fetch', + params: {}, + result: { content: 'Contact me at +1 555-010-9999' } + }; + + await skill['after_tool_call'](event); + + expect(event.result.content).not.toContain('+1 555-010-9999'); + expect(event.result.content).toContain('[PHONE_NUMBER_'); + }); + }); +});