@aictrl/hush 0.1.6 → 0.1.7
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/.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 +9 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +81 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/redact-hook.d.ts +12 -0
- package/dist/commands/redact-hook.d.ts.map +1 -0
- package/dist/commands/redact-hook.js +89 -0
- package/dist/commands/redact-hook.js.map +1 -0
- package/dist/index.js +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 +21 -0
- package/dist/plugins/opencode-hush.d.ts.map +1 -0
- package/dist/plugins/opencode-hush.js +25 -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 +19 -0
- package/examples/team-config/.codex/config.toml +4 -0
- package/examples/team-config/.opencode/plugins/hush.ts +76 -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 +107 -0
- package/src/commands/redact-hook.ts +124 -0
- package/src/index.ts +1 -1
- package/src/middleware/redactor.ts +75 -0
- package/src/plugins/opencode-hush.ts +30 -0
- package/src/plugins/sensitive-patterns.ts +71 -0
- package/src/vault/token-vault.ts +18 -4
- package/tests/init.test.ts +101 -0
- package/tests/opencode-plugin.test.ts +148 -0
- package/tests/redact-hook.test.ts +142 -0
- package/tests/redaction.test.ts +96 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Plugin: Hush PII Guard
|
|
3
|
+
*
|
|
4
|
+
* Blocks reads of sensitive files (`.env`, `*.pem`, `credentials.*`, etc.)
|
|
5
|
+
* before the tool executes — the AI model never sees the content.
|
|
6
|
+
*
|
|
7
|
+
* Defense-in-depth: works alongside the Hush proxy which redacts PII from
|
|
8
|
+
* API requests. The plugin prevents file reads; the proxy catches anything
|
|
9
|
+
* that slips through in normal files.
|
|
10
|
+
*
|
|
11
|
+
* Install: copy to `.opencode/plugins/hush.ts` and add to `opencode.json`:
|
|
12
|
+
* { "plugin": [".opencode/plugins/hush.ts"] }
|
|
13
|
+
*/
|
|
14
|
+
export declare const HushPlugin: () => Promise<{
|
|
15
|
+
'tool.execute.before': (input: {
|
|
16
|
+
tool: string;
|
|
17
|
+
}, output: {
|
|
18
|
+
args: Record<string, string>;
|
|
19
|
+
}) => Promise<void>;
|
|
20
|
+
}>;
|
|
21
|
+
//# sourceMappingURL=opencode-hush.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-hush.d.ts","sourceRoot":"","sources":["../../src/plugins/opencode-hush.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,UAAU;mCAEZ;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,UACf;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;EAU1C,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Plugin: Hush PII Guard
|
|
3
|
+
*
|
|
4
|
+
* Blocks reads of sensitive files (`.env`, `*.pem`, `credentials.*`, etc.)
|
|
5
|
+
* before the tool executes — the AI model never sees the content.
|
|
6
|
+
*
|
|
7
|
+
* Defense-in-depth: works alongside the Hush proxy which redacts PII from
|
|
8
|
+
* API requests. The plugin prevents file reads; the proxy catches anything
|
|
9
|
+
* that slips through in normal files.
|
|
10
|
+
*
|
|
11
|
+
* Install: copy to `.opencode/plugins/hush.ts` and add to `opencode.json`:
|
|
12
|
+
* { "plugin": [".opencode/plugins/hush.ts"] }
|
|
13
|
+
*/
|
|
14
|
+
import { isSensitivePath, commandReadsSensitiveFile } from './sensitive-patterns.js';
|
|
15
|
+
export const HushPlugin = async () => ({
|
|
16
|
+
'tool.execute.before': async (input, output) => {
|
|
17
|
+
if (input.tool === 'read' && isSensitivePath(output.args['filePath'] ?? '')) {
|
|
18
|
+
throw new Error('[hush] Blocked: sensitive file');
|
|
19
|
+
}
|
|
20
|
+
if (input.tool === 'bash' && commandReadsSensitiveFile(output.args['command'] ?? '')) {
|
|
21
|
+
throw new Error('[hush] Blocked: command reads sensitive file');
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=opencode-hush.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-hush.js","sourceRoot":"","sources":["../../src/plugins/opencode-hush.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAErF,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC;IACrC,qBAAqB,EAAE,KAAK,EAC1B,KAAuB,EACvB,MAAwC,EACxC,EAAE;QACF,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACpD,CAAC;QAED,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,yBAAyB,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YACrF,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for detecting sensitive file paths and commands.
|
|
3
|
+
* Used by the OpenCode hush plugin to block reads of secret files.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Check whether a file path points to a sensitive file.
|
|
7
|
+
* Matches against the basename only so absolute/relative paths both work.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isSensitivePath(filePath: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Check whether a bash command reads a sensitive file.
|
|
12
|
+
* Looks for common read commands followed by a sensitive filename.
|
|
13
|
+
*/
|
|
14
|
+
export declare function commandReadsSensitiveFile(cmd: string): boolean;
|
|
15
|
+
//# sourceMappingURL=sensitive-patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitive-patterns.d.ts","sourceRoot":"","sources":["../../src/plugins/sensitive-patterns.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAmBH;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGzD;AAUD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CA2B9D"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for detecting sensitive file paths and commands.
|
|
3
|
+
* Used by the OpenCode hush plugin to block reads of secret files.
|
|
4
|
+
*/
|
|
5
|
+
/** Glob-style patterns for files that should never be read by AI tools. */
|
|
6
|
+
const SENSITIVE_GLOBS = [
|
|
7
|
+
/^\.env($|\..*)/, // .env, .env.local, .env.production, etc.
|
|
8
|
+
/credentials/i,
|
|
9
|
+
/secret/i,
|
|
10
|
+
/\.pem$/,
|
|
11
|
+
/\.key$/,
|
|
12
|
+
/\.p12$/,
|
|
13
|
+
/\.pfx$/,
|
|
14
|
+
/\.jks$/,
|
|
15
|
+
/\.keystore$/,
|
|
16
|
+
/\.asc$/,
|
|
17
|
+
/^id_rsa/,
|
|
18
|
+
/^\.netrc$/,
|
|
19
|
+
/^\.pgpass$/,
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Check whether a file path points to a sensitive file.
|
|
23
|
+
* Matches against the basename only so absolute/relative paths both work.
|
|
24
|
+
*/
|
|
25
|
+
export function isSensitivePath(filePath) {
|
|
26
|
+
const basename = (filePath.split('/').pop() ?? '').trim();
|
|
27
|
+
return SENSITIVE_GLOBS.some((re) => re.test(basename));
|
|
28
|
+
}
|
|
29
|
+
/** Commands that read file contents (includes batcat — Ubuntu symlink for bat). */
|
|
30
|
+
const READ_COMMANDS = /\b(cat|head|tail|less|more|bat|batcat)\b/;
|
|
31
|
+
/** Strip shell metacharacters that could wrap a filename to bypass detection. */
|
|
32
|
+
function stripShellMeta(token) {
|
|
33
|
+
return token.replace(/[`"'$(){}]/g, '');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check whether a bash command reads a sensitive file.
|
|
37
|
+
* Looks for common read commands followed by a sensitive filename.
|
|
38
|
+
*/
|
|
39
|
+
export function commandReadsSensitiveFile(cmd) {
|
|
40
|
+
if (!READ_COMMANDS.test(cmd))
|
|
41
|
+
return false;
|
|
42
|
+
// Check input redirections: `cat <.env` or `cat < .env`
|
|
43
|
+
// The file after `<` is read by the preceding command.
|
|
44
|
+
const redirectPattern = /<\s*([^\s|;&<>]+)/g;
|
|
45
|
+
let rMatch;
|
|
46
|
+
while ((rMatch = redirectPattern.exec(cmd)) !== null) {
|
|
47
|
+
if (isSensitivePath(stripShellMeta(rMatch[1])))
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// Split on pipes, semicolons, &&, and redirections to get individual commands
|
|
51
|
+
const parts = cmd.split(/[|;&<>]+/);
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
const tokens = part.trim().split(/\s+/);
|
|
54
|
+
const cmdIndex = tokens.findIndex((t) => READ_COMMANDS.test(t));
|
|
55
|
+
if (cmdIndex === -1)
|
|
56
|
+
continue;
|
|
57
|
+
// Check all tokens after the command for sensitive paths (skip flags).
|
|
58
|
+
for (let i = cmdIndex + 1; i < tokens.length; i++) {
|
|
59
|
+
const token = tokens[i];
|
|
60
|
+
if (token.startsWith('-'))
|
|
61
|
+
continue; // skip flags like -n, -5
|
|
62
|
+
const cleaned = stripShellMeta(token);
|
|
63
|
+
if (isSensitivePath(cleaned))
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=sensitive-patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitive-patterns.js","sourceRoot":"","sources":["../../src/plugins/sensitive-patterns.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,2EAA2E;AAC3E,MAAM,eAAe,GAAG;IACtB,gBAAgB,EAAE,0CAA0C;IAC5D,cAAc;IACd,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,aAAa;IACb,QAAQ;IACR,SAAS;IACT,WAAW;IACX,YAAY;CACb,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,MAAM,QAAQ,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1D,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,mFAAmF;AACnF,MAAM,aAAa,GAAG,0CAA0C,CAAC;AAEjE,iFAAiF;AACjF,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACnD,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3C,wDAAwD;IACxD,uDAAuD;IACvD,MAAM,eAAe,GAAG,oBAAoB,CAAC;IAC7C,IAAI,MAAM,CAAC;IACX,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACrD,IAAI,eAAe,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;IAC/D,CAAC;IAED,8EAA8E;IAC9E,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAChE,IAAI,QAAQ,KAAK,CAAC,CAAC;YAAE,SAAS;QAE9B,uEAAuE;QACvE,KAAK,IAAI,CAAC,GAAG,QAAQ,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAClD,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC;YACzB,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS,CAAC,yBAAyB;YAC9D,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;YACtC,IAAI,eAAe,CAAC,OAAO,CAAC;gBAAE,OAAO,IAAI,CAAC;QAC5C,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-vault.d.ts","sourceRoot":"","sources":["../../src/vault/token-vault.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAgE;IAC7E,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B;;OAEG;gBACS,KAAK,GAAE,MAAuB;IAI1C;;;;OAIG;IACI,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAQpD;;;;;OAKG;IACI,SAAS,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG;IA0CjC;;;OAGG;IACI,yBAAyB,
|
|
1
|
+
{"version":3,"file":"token-vault.d.ts","sourceRoot":"","sources":["../../src/vault/token-vault.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAgE;IAC7E,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B;;OAEG;gBACS,KAAK,GAAE,MAAuB;IAI1C;;;;OAIG;IACI,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAQpD;;;;;OAKG;IACI,SAAS,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG;IA0CjC;;;OAGG;IACI,yBAAyB,KAwBtB,OAAO,MAAM,KAAG,MAAM;IAoHhC;;OAEG;IACH,OAAO,CAAC,KAAK;IASb;;;;;OAKG;IACI,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAK7C;;OAEG;IACI,KAAK,IAAI,IAAI;IAIpB;;OAEG;IACH,IAAW,IAAI,IAAI,MAAM,CAExB;CACF"}
|
|
@@ -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,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:4000"
|
|
4
|
+
},
|
|
5
|
+
"hooks": {
|
|
6
|
+
"PostToolUse": [
|
|
7
|
+
{
|
|
8
|
+
"matcher": "Bash|Read|Grep|WebFetch",
|
|
9
|
+
"hooks": [
|
|
10
|
+
{
|
|
11
|
+
"type": "command",
|
|
12
|
+
"command": "hush redact-hook",
|
|
13
|
+
"timeout": 10
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hush PII Guard — OpenCode Plugin (drop-in copy)
|
|
3
|
+
*
|
|
4
|
+
* Blocks reads of sensitive files (.env, *.pem, credentials.*, etc.)
|
|
5
|
+
* before the tool executes — the AI model never sees the content.
|
|
6
|
+
*
|
|
7
|
+
* Usage: copy this file to `.opencode/plugins/hush.ts` in your project
|
|
8
|
+
* and add to `opencode.json`:
|
|
9
|
+
* { "plugin": [".opencode/plugins/hush.ts"] }
|
|
10
|
+
*
|
|
11
|
+
* Or install from npm:
|
|
12
|
+
* import { HushPlugin } from '@aictrl/hush/opencode-plugin'
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const SENSITIVE_GLOBS = [
|
|
16
|
+
/^\.env($|\..*)/, // .env, .env.local, .env.production, etc.
|
|
17
|
+
/credentials/i,
|
|
18
|
+
/secret/i,
|
|
19
|
+
/\.pem$/,
|
|
20
|
+
/\.key$/,
|
|
21
|
+
/\.p12$/,
|
|
22
|
+
/\.pfx$/,
|
|
23
|
+
/\.jks$/,
|
|
24
|
+
/\.keystore$/,
|
|
25
|
+
/\.asc$/,
|
|
26
|
+
/^id_rsa/,
|
|
27
|
+
/^\.netrc$/,
|
|
28
|
+
/^\.pgpass$/,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function isSensitivePath(filePath: string): boolean {
|
|
32
|
+
const basename = (filePath.split('/').pop() ?? '').trim();
|
|
33
|
+
return SENSITIVE_GLOBS.some((re) => re.test(basename));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const READ_COMMANDS = /\b(cat|head|tail|less|more|bat|batcat)\b/;
|
|
37
|
+
|
|
38
|
+
function stripShellMeta(token: string): string {
|
|
39
|
+
return token.replace(/[`"'$(){}]/g, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function commandReadsSensitiveFile(cmd: string): boolean {
|
|
43
|
+
if (!READ_COMMANDS.test(cmd)) return false;
|
|
44
|
+
const redirectPattern = /<\s*([^\s|;&<>]+)/g;
|
|
45
|
+
let rMatch;
|
|
46
|
+
while ((rMatch = redirectPattern.exec(cmd)) !== null) {
|
|
47
|
+
if (isSensitivePath(stripShellMeta(rMatch[1]!))) return true;
|
|
48
|
+
}
|
|
49
|
+
const parts = cmd.split(/[|;&<>]+/);
|
|
50
|
+
for (const part of parts) {
|
|
51
|
+
const tokens = part.trim().split(/\s+/);
|
|
52
|
+
const cmdIndex = tokens.findIndex((t) => READ_COMMANDS.test(t));
|
|
53
|
+
if (cmdIndex === -1) continue;
|
|
54
|
+
for (let i = cmdIndex + 1; i < tokens.length; i++) {
|
|
55
|
+
const token = tokens[i]!;
|
|
56
|
+
if (token.startsWith('-')) continue;
|
|
57
|
+
const cleaned = stripShellMeta(token);
|
|
58
|
+
if (isSensitivePath(cleaned)) return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const HushPlugin = async () => ({
|
|
65
|
+
'tool.execute.before': async (
|
|
66
|
+
input: { tool: string },
|
|
67
|
+
output: { args: Record<string, string> },
|
|
68
|
+
) => {
|
|
69
|
+
if (input.tool === 'read' && isSensitivePath(output.args['filePath'] ?? '')) {
|
|
70
|
+
throw new Error('[hush] Blocked: sensitive file');
|
|
71
|
+
}
|
|
72
|
+
if (input.tool === 'bash' && commandReadsSensitiveFile(output.args['command'] ?? '')) {
|
|
73
|
+
throw new Error('[hush] Blocked: command reads sensitive file');
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aictrl/hush",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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"
|