@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.
Files changed (50) hide show
  1. package/.github/workflows/opencode-review.yml +52 -7
  2. package/.gitlab-ci.yml +59 -0
  3. package/README.md +150 -3
  4. package/dist/cli.js +30 -17
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/init.d.ts +11 -0
  7. package/dist/commands/init.d.ts.map +1 -0
  8. package/dist/commands/init.js +135 -0
  9. package/dist/commands/init.js.map +1 -0
  10. package/dist/commands/redact-hook.d.ts +21 -0
  11. package/dist/commands/redact-hook.d.ts.map +1 -0
  12. package/dist/commands/redact-hook.js +225 -0
  13. package/dist/commands/redact-hook.js.map +1 -0
  14. package/dist/index.js +8 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/middleware/redactor.d.ts +5 -0
  17. package/dist/middleware/redactor.d.ts.map +1 -1
  18. package/dist/middleware/redactor.js +69 -0
  19. package/dist/middleware/redactor.js.map +1 -1
  20. package/dist/plugins/opencode-hush.d.ts +32 -0
  21. package/dist/plugins/opencode-hush.d.ts.map +1 -0
  22. package/dist/plugins/opencode-hush.js +58 -0
  23. package/dist/plugins/opencode-hush.js.map +1 -0
  24. package/dist/plugins/sensitive-patterns.d.ts +15 -0
  25. package/dist/plugins/sensitive-patterns.d.ts.map +1 -0
  26. package/dist/plugins/sensitive-patterns.js +69 -0
  27. package/dist/plugins/sensitive-patterns.js.map +1 -0
  28. package/dist/vault/token-vault.d.ts.map +1 -1
  29. package/dist/vault/token-vault.js +16 -3
  30. package/dist/vault/token-vault.js.map +1 -1
  31. package/examples/team-config/.claude/settings.json +41 -0
  32. package/examples/team-config/.codex/config.toml +4 -0
  33. package/examples/team-config/.gemini/settings.json +38 -0
  34. package/examples/team-config/.opencode/plugins/hush.ts +79 -0
  35. package/examples/team-config/opencode.json +10 -0
  36. package/package.json +11 -1
  37. package/scripts/e2e-plugin-block.sh +142 -0
  38. package/scripts/e2e-proxy-live.sh +185 -0
  39. package/src/cli.ts +28 -16
  40. package/src/commands/init.ts +186 -0
  41. package/src/commands/redact-hook.ts +297 -0
  42. package/src/index.ts +7 -2
  43. package/src/middleware/redactor.ts +75 -0
  44. package/src/plugins/opencode-hush.ts +70 -0
  45. package/src/plugins/sensitive-patterns.ts +71 -0
  46. package/src/vault/token-vault.ts +18 -4
  47. package/tests/init.test.ts +255 -0
  48. package/tests/opencode-plugin.test.ts +219 -0
  49. package/tests/redact-hook.test.ts +498 -0
  50. 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
- !buf.substring(lastBracket).includes(']') &&
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":"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"}
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,4 @@
1
+ model_provider = "hush"
2
+
3
+ [model_providers.hush]
4
+ base_url = "http://127.0.0.1:4000/v1"
@@ -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
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "provider": {
3
+ "zai-coding-plan": {
4
+ "options": {
5
+ "baseURL": "http://127.0.0.1:4000/api/coding/paas/v4"
6
+ }
7
+ }
8
+ },
9
+ "plugin": [".opencode/plugins/hush.ts"]
10
+ }
package/package.json CHANGED
@@ -1,10 +1,20 @@
1
1
  {
2
2
  "name": "@aictrl/hush",
3
- "version": "0.1.6",
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 log = createLogger('hush-cli');
6
- const PORT = process.env.PORT || 4000;
3
+ const subcommand = process.argv[2];
7
4
 
8
- const server = app.listen(PORT, () => {
9
- log.info(`Hush Semantic Gateway is listening on http://localhost:${PORT}`);
10
- log.info(`Routes: /v1/messages → Anthropic, /v1/chat/completions → OpenAI, /api/paas/v4/** → ZhipuAI, * → Google`);
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
- server.on('error', (err: NodeJS.ErrnoException) => {
14
- if (err.code === 'EADDRINUSE') {
15
- log.error(`Port ${PORT} is already in use. Stop the other process or use PORT=<number> hush`);
16
- } else {
17
- log.error({ err }, 'Failed to start server');
18
- }
19
- process.exit(1);
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
+ }