@aictrl/hush 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/opencode-review.yml +52 -7
- package/.gitlab-ci.yml +59 -0
- package/README.md +150 -3
- package/dist/cli.js +30 -17
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +135 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/redact-hook.d.ts +21 -0
- package/dist/commands/redact-hook.d.ts.map +1 -0
- package/dist/commands/redact-hook.js +225 -0
- package/dist/commands/redact-hook.js.map +1 -0
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/redactor.d.ts +5 -0
- package/dist/middleware/redactor.d.ts.map +1 -1
- package/dist/middleware/redactor.js +69 -0
- package/dist/middleware/redactor.js.map +1 -1
- package/dist/plugins/opencode-hush.d.ts +32 -0
- package/dist/plugins/opencode-hush.d.ts.map +1 -0
- package/dist/plugins/opencode-hush.js +58 -0
- package/dist/plugins/opencode-hush.js.map +1 -0
- package/dist/plugins/sensitive-patterns.d.ts +15 -0
- package/dist/plugins/sensitive-patterns.d.ts.map +1 -0
- package/dist/plugins/sensitive-patterns.js +69 -0
- package/dist/plugins/sensitive-patterns.js.map +1 -0
- package/dist/vault/token-vault.d.ts.map +1 -1
- package/dist/vault/token-vault.js +16 -3
- package/dist/vault/token-vault.js.map +1 -1
- package/examples/team-config/.claude/settings.json +41 -0
- package/examples/team-config/.codex/config.toml +4 -0
- package/examples/team-config/.gemini/settings.json +38 -0
- package/examples/team-config/.opencode/plugins/hush.ts +79 -0
- package/examples/team-config/opencode.json +10 -0
- package/package.json +11 -1
- package/scripts/e2e-plugin-block.sh +142 -0
- package/scripts/e2e-proxy-live.sh +185 -0
- package/src/cli.ts +28 -16
- package/src/commands/init.ts +186 -0
- package/src/commands/redact-hook.ts +297 -0
- package/src/index.ts +7 -2
- package/src/middleware/redactor.ts +75 -0
- package/src/plugins/opencode-hush.ts +70 -0
- package/src/plugins/sensitive-patterns.ts +71 -0
- package/src/vault/token-vault.ts +18 -4
- package/tests/init.test.ts +255 -0
- package/tests/opencode-plugin.test.ts +219 -0
- package/tests/redact-hook.test.ts +498 -0
- package/tests/redaction.test.ts +96 -0
|
@@ -78,7 +78,9 @@ export class TokenVault {
|
|
|
78
78
|
createStreamingRehydrator() {
|
|
79
79
|
let buffer = '';
|
|
80
80
|
const maxTokenLen = Math.max(...[...this.vault.keys()].map(t => t.length), 0);
|
|
81
|
-
// Accumulate content fields across SSE events to reassemble split tokens
|
|
81
|
+
// Accumulate content fields across SSE events to reassemble split tokens.
|
|
82
|
+
// Cap buffer size to prevent unbounded memory growth on very long streams.
|
|
83
|
+
const MAX_BUFFER_SIZE = 1024 * 1024; // 1 MB per field
|
|
82
84
|
const contentBuffers = {};
|
|
83
85
|
const CONTENT_FIELDS = ['content', 'reasoning_content', 'partial_json'];
|
|
84
86
|
const rehydrateText = (text) => {
|
|
@@ -167,11 +169,22 @@ export class TokenVault {
|
|
|
167
169
|
continue;
|
|
168
170
|
const bufKey = actualField;
|
|
169
171
|
contentBuffers[bufKey] = (contentBuffers[bufKey] || '') + target[actualField];
|
|
172
|
+
// Cap buffer size: flush everything if it grows too large
|
|
173
|
+
if (contentBuffers[bufKey].length > MAX_BUFFER_SIZE) {
|
|
174
|
+
target[actualField] = flushField(bufKey);
|
|
175
|
+
modified = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
170
178
|
const buf = contentBuffers[bufKey];
|
|
171
179
|
const lastBracket = buf.lastIndexOf('[');
|
|
180
|
+
// Only treat as partial token if the text after '[' looks like a
|
|
181
|
+
// token prefix (uppercase letter or underscore), not JSON array content.
|
|
182
|
+
// Also hold back a bare '[' at the end — not enough chars yet to decide.
|
|
183
|
+
const tail = lastBracket >= 0 ? buf.substring(lastBracket) : '';
|
|
172
184
|
const hasPartialToken = maxTokenLen > 0 && lastBracket >= 0 &&
|
|
173
|
-
!
|
|
174
|
-
buf.length - lastBracket < maxTokenLen
|
|
185
|
+
!tail.includes(']') &&
|
|
186
|
+
buf.length - lastBracket < maxTokenLen &&
|
|
187
|
+
(tail === '[' || /^\[[A-Z_]/.test(tail));
|
|
175
188
|
if (hasPartialToken) {
|
|
176
189
|
const safe = buf.substring(0, lastBracket);
|
|
177
190
|
contentBuffers[bufKey] = buf.substring(lastBracket);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-vault.js","sourceRoot":"","sources":["../../src/vault/token-vault.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,OAAO,UAAU;IACb,KAAK,GAAsD,IAAI,GAAG,EAAE,CAAC;IAC5D,GAAG,CAAS;IAE7B;;OAEG;IACH,YAAY,QAAgB,EAAE,GAAG,EAAE,GAAG,IAAI;QACxC,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC;IACnB,CAAC;IAED;;;;OAIG;IACI,UAAU,CAAC,MAA2B;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;YACpC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QACnD,CAAC;QACD,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;IAED;;;;;OAKG;IACI,SAAS,CAAC,KAAU;QACzB,qCAAqC;QACrC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAExC,wEAAwE;QACxE,IAAI,MAAW,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,GAAG,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;gBAClD,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC;gBACxB,CAAC,CAAC,KAAK,CAAC;QACZ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,GAAG,KAAK,CAAC;QACjB,CAAC;QAED,MAAM,OAAO,GAAG,CAAC,IAAS,EAAO,EAAE;YACjC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC7B,IAAI,IAAI,GAAG,IAAI,CAAC;gBAChB,iCAAiC;gBACjC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;oBAClD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC7C,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YAED,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxB,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YACzC,CAAC;YAED,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC9C,MAAM,GAAG,GAAQ,EAAE,CAAC;gBACpB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;oBAChD,GAAG,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC5B,CAAC;gBACD,OAAO,GAAG,CAAC;YACb,CAAC;YAED,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QAEF,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC;IAED;;;OAGG;IACI,yBAAyB;QAC9B,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QAE9E,
|
|
1
|
+
{"version":3,"file":"token-vault.js","sourceRoot":"","sources":["../../src/vault/token-vault.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:4000"
|
|
4
|
+
},
|
|
5
|
+
"hooks": {
|
|
6
|
+
"PreToolUse": [
|
|
7
|
+
{
|
|
8
|
+
"matcher": "mcp__.*",
|
|
9
|
+
"hooks": [
|
|
10
|
+
{
|
|
11
|
+
"type": "command",
|
|
12
|
+
"command": "hush redact-hook",
|
|
13
|
+
"timeout": 10
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"PostToolUse": [
|
|
19
|
+
{
|
|
20
|
+
"matcher": "Bash|Read|Grep|WebFetch",
|
|
21
|
+
"hooks": [
|
|
22
|
+
{
|
|
23
|
+
"type": "command",
|
|
24
|
+
"command": "hush redact-hook",
|
|
25
|
+
"timeout": 10
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"matcher": "mcp__.*",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "hush redact-hook",
|
|
35
|
+
"timeout": 10
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"BeforeTool": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "mcp__.*",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "hush redact-hook",
|
|
10
|
+
"timeout": 10
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"AfterTool": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "run_shell_command|read_file|read_many_files|search_file_content|web_fetch",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "hush redact-hook",
|
|
22
|
+
"timeout": 10
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"matcher": "mcp__.*",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "hush redact-hook",
|
|
32
|
+
"timeout": 10
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hush PII Guard — OpenCode Plugin (drop-in copy)
|
|
3
|
+
*
|
|
4
|
+
* This drop-in copy provides file-blocking only (sensitive file reads).
|
|
5
|
+
* For full bidirectional PII redaction (tool args + tool results),
|
|
6
|
+
* install from npm instead:
|
|
7
|
+
*
|
|
8
|
+
* npm install @aictrl/hush
|
|
9
|
+
*
|
|
10
|
+
* Then in your plugin entry point:
|
|
11
|
+
* import { HushPlugin } from '@aictrl/hush/opencode-plugin'
|
|
12
|
+
*
|
|
13
|
+
* Usage (drop-in): copy this file to `.opencode/plugins/hush.ts` in your
|
|
14
|
+
* project and add to `opencode.json`:
|
|
15
|
+
* { "plugin": [".opencode/plugins/hush.ts"] }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const SENSITIVE_GLOBS = [
|
|
19
|
+
/^\.env($|\..*)/, // .env, .env.local, .env.production, etc.
|
|
20
|
+
/credentials/i,
|
|
21
|
+
/secret/i,
|
|
22
|
+
/\.pem$/,
|
|
23
|
+
/\.key$/,
|
|
24
|
+
/\.p12$/,
|
|
25
|
+
/\.pfx$/,
|
|
26
|
+
/\.jks$/,
|
|
27
|
+
/\.keystore$/,
|
|
28
|
+
/\.asc$/,
|
|
29
|
+
/^id_rsa/,
|
|
30
|
+
/^\.netrc$/,
|
|
31
|
+
/^\.pgpass$/,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function isSensitivePath(filePath: string): boolean {
|
|
35
|
+
const basename = (filePath.split('/').pop() ?? '').trim();
|
|
36
|
+
return SENSITIVE_GLOBS.some((re) => re.test(basename));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const READ_COMMANDS = /\b(cat|head|tail|less|more|bat|batcat)\b/;
|
|
40
|
+
|
|
41
|
+
function stripShellMeta(token: string): string {
|
|
42
|
+
return token.replace(/[`"'$(){}]/g, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function commandReadsSensitiveFile(cmd: string): boolean {
|
|
46
|
+
if (!READ_COMMANDS.test(cmd)) return false;
|
|
47
|
+
const redirectPattern = /<\s*([^\s|;&<>]+)/g;
|
|
48
|
+
let rMatch;
|
|
49
|
+
while ((rMatch = redirectPattern.exec(cmd)) !== null) {
|
|
50
|
+
if (isSensitivePath(stripShellMeta(rMatch[1]!))) return true;
|
|
51
|
+
}
|
|
52
|
+
const parts = cmd.split(/[|;&<>]+/);
|
|
53
|
+
for (const part of parts) {
|
|
54
|
+
const tokens = part.trim().split(/\s+/);
|
|
55
|
+
const cmdIndex = tokens.findIndex((t) => READ_COMMANDS.test(t));
|
|
56
|
+
if (cmdIndex === -1) continue;
|
|
57
|
+
for (let i = cmdIndex + 1; i < tokens.length; i++) {
|
|
58
|
+
const token = tokens[i]!;
|
|
59
|
+
if (token.startsWith('-')) continue;
|
|
60
|
+
const cleaned = stripShellMeta(token);
|
|
61
|
+
if (isSensitivePath(cleaned)) return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const HushPlugin = async () => ({
|
|
68
|
+
'tool.execute.before': async (
|
|
69
|
+
input: { tool: string },
|
|
70
|
+
output: { args: Record<string, string> },
|
|
71
|
+
) => {
|
|
72
|
+
if (input.tool === 'read' && isSensitivePath(output.args['filePath'] ?? '')) {
|
|
73
|
+
throw new Error('[hush] Blocked: sensitive file');
|
|
74
|
+
}
|
|
75
|
+
if (input.tool === 'bash' && commandReadsSensitiveFile(output.args['command'] ?? '')) {
|
|
76
|
+
throw new Error('[hush] Blocked: command reads sensitive file');
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aictrl/hush",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Hush: A Semantic Security Gateway for AI Agents. Redacts PII from prompts and tool outputs locally before they hit the cloud.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./opencode-plugin": {
|
|
14
|
+
"import": "./dist/plugins/opencode-hush.js",
|
|
15
|
+
"types": "./dist/plugins/opencode-hush.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
8
18
|
"bin": {
|
|
9
19
|
"hush": "dist/cli.js"
|
|
10
20
|
},
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# E2E Scenario A: OpenCode hush plugin blocks .env read
|
|
4
|
+
#
|
|
5
|
+
# Verifies that the hush plugin's tool.execute.before hook prevents
|
|
6
|
+
# the AI model from ever reading sensitive files. The model should
|
|
7
|
+
# receive a "blocked" error instead of the file contents.
|
|
8
|
+
#
|
|
9
|
+
# Usage: ./scripts/e2e-plugin-block.sh
|
|
10
|
+
# Requirements: opencode CLI, node
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
16
|
+
|
|
17
|
+
# Colors
|
|
18
|
+
RED='\033[0;31m'
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
YELLOW='\033[1;33m'
|
|
21
|
+
CYAN='\033[0;36m'
|
|
22
|
+
NC='\033[0m'
|
|
23
|
+
|
|
24
|
+
PASS_COUNT=0
|
|
25
|
+
FAIL_COUNT=0
|
|
26
|
+
WORK_DIR=""
|
|
27
|
+
|
|
28
|
+
cleanup() {
|
|
29
|
+
echo ""
|
|
30
|
+
echo -e "${CYAN}Cleaning up...${NC}"
|
|
31
|
+
[ -n "$WORK_DIR" ] && rm -rf "$WORK_DIR"
|
|
32
|
+
}
|
|
33
|
+
trap cleanup EXIT
|
|
34
|
+
|
|
35
|
+
pass() {
|
|
36
|
+
PASS_COUNT=$((PASS_COUNT + 1))
|
|
37
|
+
echo -e " ${GREEN}PASS${NC} $1"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fail() {
|
|
41
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
42
|
+
echo -e " ${RED}FAIL${NC} $1"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
assert_contains() {
|
|
46
|
+
local haystack="$1" needle="$2" msg="$3"
|
|
47
|
+
if echo "$haystack" | grep -qiF "$needle"; then
|
|
48
|
+
pass "$msg"
|
|
49
|
+
else
|
|
50
|
+
fail "$msg (expected to find '$needle')"
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
assert_not_contains() {
|
|
55
|
+
local haystack="$1" needle="$2" msg="$3"
|
|
56
|
+
if echo "$haystack" | grep -qiF "$needle"; then
|
|
57
|
+
fail "$msg (found '$needle' which should have been blocked)"
|
|
58
|
+
else
|
|
59
|
+
pass "$msg"
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
echo -e "${CYAN}================================================${NC}"
|
|
64
|
+
echo -e "${CYAN} E2E Scenario A: Plugin Blocks .env Read ${NC}"
|
|
65
|
+
echo -e "${CYAN}================================================${NC}"
|
|
66
|
+
echo ""
|
|
67
|
+
|
|
68
|
+
# --- Step 1: Create temp project with .env and hush plugin ---
|
|
69
|
+
echo -e "${YELLOW}[1/4] Creating temp project with .env and hush plugin...${NC}"
|
|
70
|
+
|
|
71
|
+
WORK_DIR=$(mktemp -d)
|
|
72
|
+
mkdir -p "$WORK_DIR/.opencode/plugins"
|
|
73
|
+
|
|
74
|
+
# Sensitive .env file with PII
|
|
75
|
+
cat > "$WORK_DIR/.env" <<'ENVEOF'
|
|
76
|
+
DATABASE_URL=postgres://admin:supersecret@10.42.99.7:5432/prod
|
|
77
|
+
API_KEY=sk-live-a1b2c3d4e5f6g7h8i9j0k1l2m3n4
|
|
78
|
+
ADMIN_EMAIL=alice@confidential-corp.com
|
|
79
|
+
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
|
80
|
+
ENVEOF
|
|
81
|
+
|
|
82
|
+
# Copy the drop-in plugin
|
|
83
|
+
cp "$PROJECT_DIR/examples/team-config/.opencode/plugins/hush.ts" \
|
|
84
|
+
"$WORK_DIR/.opencode/plugins/hush.ts"
|
|
85
|
+
|
|
86
|
+
# opencode.json — point at real provider + enable plugin
|
|
87
|
+
cat > "$WORK_DIR/opencode.json" <<OCEOF
|
|
88
|
+
{
|
|
89
|
+
"provider": {
|
|
90
|
+
"zai-coding-plan": {
|
|
91
|
+
"options": {
|
|
92
|
+
"baseURL": "https://open.bigmodel.cn/api/coding/paas/v4"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"plugin": [".opencode/plugins/hush.ts"]
|
|
97
|
+
}
|
|
98
|
+
OCEOF
|
|
99
|
+
|
|
100
|
+
echo -e " Temp project: ${WORK_DIR}"
|
|
101
|
+
echo -e " .env contains: email, API key, DB URL, AWS secret"
|
|
102
|
+
|
|
103
|
+
# --- Step 2: Run OpenCode with prompt to read .env ---
|
|
104
|
+
echo ""
|
|
105
|
+
echo -e "${YELLOW}[2/4] Running OpenCode: 'read the file .env and tell me what's in it'...${NC}"
|
|
106
|
+
|
|
107
|
+
cd "$WORK_DIR"
|
|
108
|
+
OUTPUT=$(timeout 120 opencode -p "read the file .env and tell me what's in it" -q -f json 2>&1) || true
|
|
109
|
+
echo -e " Output length: $(echo "$OUTPUT" | wc -c) bytes"
|
|
110
|
+
|
|
111
|
+
# --- Step 3: Verify plugin blocked the read ---
|
|
112
|
+
echo ""
|
|
113
|
+
echo -e "${YELLOW}[3/4] Verifying plugin blocked .env read...${NC}"
|
|
114
|
+
echo ""
|
|
115
|
+
|
|
116
|
+
# The output should mention blocking/error, not contain the actual PII
|
|
117
|
+
assert_contains "$OUTPUT" "block" "Output mentions blocking"
|
|
118
|
+
|
|
119
|
+
# --- Step 4: Verify PII never appears in output ---
|
|
120
|
+
echo ""
|
|
121
|
+
echo -e "${YELLOW}[4/4] Verifying PII never appears in output...${NC}"
|
|
122
|
+
echo ""
|
|
123
|
+
|
|
124
|
+
assert_not_contains "$OUTPUT" "alice@confidential-corp.com" "Email not in output"
|
|
125
|
+
assert_not_contains "$OUTPUT" "sk-live-a1b2c3d4e5f6g7h8i9j0k1l2m3n4" "API key not in output"
|
|
126
|
+
assert_not_contains "$OUTPUT" "supersecret" "DB password not in output"
|
|
127
|
+
assert_not_contains "$OUTPUT" "wJalrXUtnFEMI" "AWS secret not in output"
|
|
128
|
+
|
|
129
|
+
# --- Summary ---
|
|
130
|
+
echo ""
|
|
131
|
+
echo -e "${CYAN}================================================${NC}"
|
|
132
|
+
TOTAL=$((PASS_COUNT + FAIL_COUNT))
|
|
133
|
+
if [ "$FAIL_COUNT" -eq 0 ]; then
|
|
134
|
+
echo -e "${GREEN} ALL ${TOTAL} CHECKS PASSED${NC}"
|
|
135
|
+
echo ""
|
|
136
|
+
echo -e " ${GREEN}Plugin blocked .env read — PII never reached the model.${NC}"
|
|
137
|
+
else
|
|
138
|
+
echo -e "${RED} ${FAIL_COUNT}/${TOTAL} CHECKS FAILED${NC}"
|
|
139
|
+
fi
|
|
140
|
+
echo -e "${CYAN}================================================${NC}"
|
|
141
|
+
|
|
142
|
+
exit "$FAIL_COUNT"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# E2E Scenario B: Proxy redacts PII from normal file reads
|
|
4
|
+
#
|
|
5
|
+
# A non-sensitive filename (config.txt) containing PII gets through the
|
|
6
|
+
# plugin's filename check. The hush proxy intercepts the API request and
|
|
7
|
+
# redacts PII before it reaches the LLM provider.
|
|
8
|
+
#
|
|
9
|
+
# Usage: ./scripts/e2e-proxy-live.sh
|
|
10
|
+
# Requirements: opencode CLI, node, npm (dependencies installed + built)
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
16
|
+
|
|
17
|
+
# Colors
|
|
18
|
+
RED='\033[0;31m'
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
YELLOW='\033[1;33m'
|
|
21
|
+
CYAN='\033[0;36m'
|
|
22
|
+
NC='\033[0m'
|
|
23
|
+
|
|
24
|
+
GATEWAY_PORT=4000
|
|
25
|
+
GATEWAY_PID=""
|
|
26
|
+
PASS_COUNT=0
|
|
27
|
+
FAIL_COUNT=0
|
|
28
|
+
WORK_DIR=""
|
|
29
|
+
|
|
30
|
+
cleanup() {
|
|
31
|
+
echo ""
|
|
32
|
+
echo -e "${CYAN}Cleaning up...${NC}"
|
|
33
|
+
[ -n "$GATEWAY_PID" ] && kill "$GATEWAY_PID" 2>/dev/null || true
|
|
34
|
+
[ -n "$WORK_DIR" ] && rm -rf "$WORK_DIR"
|
|
35
|
+
wait 2>/dev/null || true
|
|
36
|
+
}
|
|
37
|
+
trap cleanup EXIT
|
|
38
|
+
|
|
39
|
+
pass() {
|
|
40
|
+
PASS_COUNT=$((PASS_COUNT + 1))
|
|
41
|
+
echo -e " ${GREEN}PASS${NC} $1"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fail() {
|
|
45
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
46
|
+
echo -e " ${RED}FAIL${NC} $1"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
assert_contains() {
|
|
50
|
+
local haystack="$1" needle="$2" msg="$3"
|
|
51
|
+
if echo "$haystack" | grep -qiF "$needle"; then
|
|
52
|
+
pass "$msg"
|
|
53
|
+
else
|
|
54
|
+
fail "$msg (expected to find '$needle')"
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
assert_not_contains() {
|
|
59
|
+
local haystack="$1" needle="$2" msg="$3"
|
|
60
|
+
if echo "$haystack" | grep -qiF "$needle"; then
|
|
61
|
+
fail "$msg (found '$needle' which should have been redacted)"
|
|
62
|
+
else
|
|
63
|
+
pass "$msg"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
wait_for_port() {
|
|
68
|
+
local port=$1 label=$2 max_attempts=${3:-20}
|
|
69
|
+
for i in $(seq 1 "$max_attempts"); do
|
|
70
|
+
if curl -sf "http://127.0.0.1:${port}/health" > /dev/null 2>&1; then
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
sleep 0.5
|
|
74
|
+
done
|
|
75
|
+
echo -e "${RED}${label} failed to start on :${port}${NC}"
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
echo -e "${CYAN}================================================${NC}"
|
|
80
|
+
echo -e "${CYAN} E2E Scenario B: Proxy Redacts PII in Normal ${NC}"
|
|
81
|
+
echo -e "${CYAN} File (Plugin Allows, Proxy Catches) ${NC}"
|
|
82
|
+
echo -e "${CYAN}================================================${NC}"
|
|
83
|
+
echo ""
|
|
84
|
+
|
|
85
|
+
cd "$PROJECT_DIR"
|
|
86
|
+
|
|
87
|
+
# --- Step 1: Start Hush gateway ---
|
|
88
|
+
echo -e "${YELLOW}[1/5] Starting Hush gateway on :${GATEWAY_PORT}...${NC}"
|
|
89
|
+
|
|
90
|
+
PORT=$GATEWAY_PORT DEBUG=true node dist/cli.js > /tmp/hush-e2e-proxy.log 2>&1 &
|
|
91
|
+
GATEWAY_PID=$!
|
|
92
|
+
|
|
93
|
+
wait_for_port "$GATEWAY_PORT" "Gateway" || exit 1
|
|
94
|
+
echo -e " Gateway PID: ${GATEWAY_PID}"
|
|
95
|
+
|
|
96
|
+
# --- Step 2: Create temp project with config.txt containing PII ---
|
|
97
|
+
echo -e "${YELLOW}[2/5] Creating temp project with config.txt (PII in normal file)...${NC}"
|
|
98
|
+
|
|
99
|
+
WORK_DIR=$(mktemp -d)
|
|
100
|
+
mkdir -p "$WORK_DIR/.opencode/plugins"
|
|
101
|
+
|
|
102
|
+
# Normal filename — plugin won't block this
|
|
103
|
+
cat > "$WORK_DIR/config.txt" <<'CFGEOF'
|
|
104
|
+
# Application Configuration
|
|
105
|
+
app_name: MyApp
|
|
106
|
+
admin_contact: alice@confidential-corp.com
|
|
107
|
+
server_ip: 10.42.99.7
|
|
108
|
+
api_key=sk-live-a1b2c3d4e5f6g7h8i9j0k1l2m3n4
|
|
109
|
+
log_level: info
|
|
110
|
+
CFGEOF
|
|
111
|
+
|
|
112
|
+
# Copy the hush plugin (it won't block config.txt — not a sensitive filename)
|
|
113
|
+
cp "$PROJECT_DIR/examples/team-config/.opencode/plugins/hush.ts" \
|
|
114
|
+
"$WORK_DIR/.opencode/plugins/hush.ts"
|
|
115
|
+
|
|
116
|
+
# Point OpenCode at hush proxy
|
|
117
|
+
cat > "$WORK_DIR/opencode.json" <<OCEOF
|
|
118
|
+
{
|
|
119
|
+
"provider": {
|
|
120
|
+
"zai-coding-plan": {
|
|
121
|
+
"options": {
|
|
122
|
+
"baseURL": "http://127.0.0.1:${GATEWAY_PORT}/api/coding/paas/v4"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"plugin": [".opencode/plugins/hush.ts"]
|
|
127
|
+
}
|
|
128
|
+
OCEOF
|
|
129
|
+
|
|
130
|
+
echo -e " Temp project: ${WORK_DIR}"
|
|
131
|
+
|
|
132
|
+
# --- Step 3: Check vault is empty before test ---
|
|
133
|
+
echo -e "${YELLOW}[3/5] Checking gateway vault is empty before test...${NC}"
|
|
134
|
+
|
|
135
|
+
HEALTH_BEFORE=$(curl -sf "http://127.0.0.1:${GATEWAY_PORT}/health")
|
|
136
|
+
VAULT_BEFORE=$(echo "$HEALTH_BEFORE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('vaultSize', 0))" 2>/dev/null || echo "0")
|
|
137
|
+
echo -e " Vault size before: ${VAULT_BEFORE}"
|
|
138
|
+
|
|
139
|
+
# --- Step 4: Run OpenCode to read config.txt ---
|
|
140
|
+
echo -e "${YELLOW}[4/5] Running OpenCode: 'read config.txt and summarize it'...${NC}"
|
|
141
|
+
|
|
142
|
+
cd "$WORK_DIR"
|
|
143
|
+
OUTPUT=$(timeout 120 opencode -p "read config.txt and summarize it" -q -f json 2>&1) || true
|
|
144
|
+
echo -e " Output length: $(echo "$OUTPUT" | wc -c) bytes"
|
|
145
|
+
|
|
146
|
+
# --- Step 5: Verify proxy redacted PII ---
|
|
147
|
+
echo ""
|
|
148
|
+
echo -e "${YELLOW}[5/5] Verifying proxy intercepted PII...${NC}"
|
|
149
|
+
echo ""
|
|
150
|
+
|
|
151
|
+
# Check vault has tokens
|
|
152
|
+
HEALTH_AFTER=$(curl -sf "http://127.0.0.1:${GATEWAY_PORT}/health")
|
|
153
|
+
VAULT_AFTER=$(echo "$HEALTH_AFTER" | python3 -c "import sys, json; print(json.load(sys.stdin).get('vaultSize', 0))" 2>/dev/null || echo "0")
|
|
154
|
+
echo -e " Vault size after: ${VAULT_AFTER}"
|
|
155
|
+
|
|
156
|
+
if [ "$VAULT_AFTER" -gt 0 ]; then
|
|
157
|
+
pass "Vault contains ${VAULT_AFTER} token(s) — PII was intercepted by proxy"
|
|
158
|
+
else
|
|
159
|
+
fail "Vault is empty (expected > 0 tokens)"
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Check gateway logs for redaction
|
|
163
|
+
GATEWAY_LOG=$(cat /tmp/hush-e2e-proxy.log 2>/dev/null || echo "")
|
|
164
|
+
if echo "$GATEWAY_LOG" | grep -qi "redact"; then
|
|
165
|
+
pass "Gateway logs show redaction activity"
|
|
166
|
+
else
|
|
167
|
+
fail "Gateway logs don't show redaction (may not be an error if log format changed)"
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# --- Summary ---
|
|
171
|
+
echo ""
|
|
172
|
+
echo -e "${CYAN}================================================${NC}"
|
|
173
|
+
TOTAL=$((PASS_COUNT + FAIL_COUNT))
|
|
174
|
+
if [ "$FAIL_COUNT" -eq 0 ]; then
|
|
175
|
+
echo -e "${GREEN} ALL ${TOTAL} CHECKS PASSED${NC}"
|
|
176
|
+
echo ""
|
|
177
|
+
echo -e " ${GREEN}Plugin allowed config.txt (not a sensitive filename).${NC}"
|
|
178
|
+
echo -e " ${GREEN}Proxy caught PII in the API request and redacted it.${NC}"
|
|
179
|
+
echo -e " ${GREEN}Defense-in-depth: plugin + proxy working together.${NC}"
|
|
180
|
+
else
|
|
181
|
+
echo -e "${RED} ${FAIL_COUNT}/${TOTAL} CHECKS FAILED${NC}"
|
|
182
|
+
fi
|
|
183
|
+
echo -e "${CYAN}================================================${NC}"
|
|
184
|
+
|
|
185
|
+
exit "$FAIL_COUNT"
|
package/src/cli.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { app } from './index.js';
|
|
3
|
-
import { createLogger } from './lib/logger.js';
|
|
4
2
|
|
|
5
|
-
const
|
|
6
|
-
const PORT = process.env.PORT || 4000;
|
|
3
|
+
const subcommand = process.argv[2];
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
})
|
|
5
|
+
if (subcommand === 'redact-hook') {
|
|
6
|
+
const { run } = await import('./commands/redact-hook.js');
|
|
7
|
+
await run();
|
|
8
|
+
} else if (subcommand === 'init') {
|
|
9
|
+
const { run } = await import('./commands/init.js');
|
|
10
|
+
run(process.argv.slice(3));
|
|
11
|
+
} else {
|
|
12
|
+
// Default: start the proxy server
|
|
13
|
+
const { app } = await import('./index.js');
|
|
14
|
+
const { createLogger } = await import('./lib/logger.js');
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
log.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
const log = createLogger('hush-cli');
|
|
17
|
+
const PORT = process.env.PORT || 4000;
|
|
18
|
+
|
|
19
|
+
const server = app.listen(PORT, () => {
|
|
20
|
+
log.info(`Hush Semantic Gateway is listening on http://localhost:${PORT}`);
|
|
21
|
+
log.info(`Routes: /v1/messages → Anthropic, /v1/chat/completions → OpenAI, /api/paas/v4/** → ZhipuAI, * → Google`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
25
|
+
if (err.code === 'EADDRINUSE') {
|
|
26
|
+
log.error(`Port ${PORT} is already in use. Stop the other process or use PORT=<number> hush`);
|
|
27
|
+
} else {
|
|
28
|
+
log.error({ err }, 'Failed to start server');
|
|
29
|
+
}
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
}
|