@hynaweb/kavlr 1.0.0 โ 1.0.3
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/README.md +30 -0
- package/bin/cli.js +178 -86
- package/package.json +6 -10
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ๐ก๏ธ KAVLR: The Agent Leash
|
|
2
|
+
**Runtime security for AI coding agents. Built on a $150 phone.**
|
|
3
|
+
|
|
4
|
+
KAVLR is a fail-closed Bash hook that intercepts dangerous terminal commands (like `rm -rf`, `cat .env`, `sudo`) executed by AI agents (Claude Code, Cursor, Devin) *before* the OS runs them.
|
|
5
|
+
|
|
6
|
+
If the API is unreachable, it defaults to **BLOCKED**. Safety first. Always.
|
|
7
|
+
|
|
8
|
+
### โก Install in 30 Seconds
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @hynaweb/kavlr
|
|
11
|
+
javlr init
|
|
12
|
+
```
|
|
13
|
+
*(Paste your API key from [kavlr.netlify.app](https://kavlr.netlify.app) when prompted)*
|
|
14
|
+
|
|
15
|
+
### ๐งช Test It
|
|
16
|
+
Try to read your environment variables:
|
|
17
|
+
```bash
|
|
18
|
+
cat .env
|
|
19
|
+
```
|
|
20
|
+
**Expected Output:** `๐ [BLOCKED] THREAT DETECTED`
|
|
21
|
+
|
|
22
|
+
*(Pro Tip: Need to bypass it for debugging? Just use a backslash: `\cat .env`)*
|
|
23
|
+
|
|
24
|
+
### ๐ฅ Why I Built This
|
|
25
|
+
Iโm a 12-year-old developer building entirely on a $150 Android phone. I realized AI agents can halucinate and destroy production environments with zero "Are you sure?" prompts. Enterprise tools cost thousands. KAVLR is free, lightweight, and fail-closed.
|
|
26
|
+
|
|
27
|
+
[Read the full story of how my own code locked me out for 48 hours โ](https://kavlr.netlify.app) *(Link to your Dev.to post later, or a Notion doc for now)*
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
**Tech Stack:** Vanilla JS, Supabase Edge Functions (Deno), PostgreSQL (RLS), Bash.
|
package/bin/cli.js
CHANGED
|
@@ -1,106 +1,198 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const API_URL = 'https://yasllkvdrgnqtjesdyfz.supabase.co/functions/v1/intercept';
|
|
10
|
-
const args = process.argv.slice(2);
|
|
11
|
-
const command = args[0];
|
|
12
|
-
|
|
13
|
-
if (command === 'init') {
|
|
14
|
-
console.log('\x1b[36m%s\x1b[0m', '๐ก๏ธ Initializing KAVLR...');
|
|
15
|
-
const shell = process.env.SHELL || '';
|
|
16
|
-
const rcFile = shell.includes('zsh') ? '.zshrc' : '.bashrc';
|
|
17
|
-
const rcPath = path.join(os.homedir(), rcFile);
|
|
18
|
-
|
|
19
|
-
const rl = readline.createInterface({
|
|
20
|
-
input: process.stdin,
|
|
21
|
-
output: process.stdout
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
rl.question('\n\x1b[33mPlease enter your KAVLR API Key: \x1b[0m', (apiKey) => {
|
|
25
|
-
if (!apiKey) {
|
|
26
|
-
console.log('\x1b[31m%s\x1b[0m', 'โ API Key is required. Setup aborted.');
|
|
27
|
-
rl.close();
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
6
|
+
const { execSync, spawn } = require('child_process');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { program } = require('commander');
|
|
30
9
|
|
|
31
|
-
|
|
10
|
+
const CONFIG_FILE = path.join(os.homedir(), '.kavlr_config');
|
|
11
|
+
const SUPABASE_URL = "https://yasllkvdrgnqtjesdyfz.supabase.co";
|
|
12
|
+
const SUPABASE_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inlhc2xsa3ZkcmducXRqZXNkeWZ6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3OTIyODQwNjgsImV4cCI6MjEwNzg2MDA2OH0.1bD80IRX7agRAK7luYzdsUvFO3uSbfaK1OSo58l4-N4";
|
|
32
13
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.log('\n\x1b[32m%s\x1b[0m', 'โ
KAVLR is already configured. (If you need to change your key, edit ~/' + rcFile + ')');
|
|
41
|
-
}
|
|
14
|
+
function loadKey() {
|
|
15
|
+
if (process.env.KAVLR_API_KEY) return process.env.KAVLR_API_KEY;
|
|
16
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
19
|
+
return data.api_key;
|
|
20
|
+
} catch (e) {}
|
|
42
21
|
}
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return;
|
|
22
|
+
return null;
|
|
47
23
|
}
|
|
48
24
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const payload = originalCmd + ' ' + originalArgs;
|
|
53
|
-
|
|
54
|
-
analyzeCommand(payload, true, originalCmd, originalArgs, true);
|
|
55
|
-
return;
|
|
25
|
+
function saveKey(key) {
|
|
26
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ api_key: key }), { mode: 0o600 });
|
|
27
|
+
console.log(chalk.green(`โ
API Key saved securely to ${CONFIG_FILE}`));
|
|
56
28
|
}
|
|
57
29
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
30
|
+
function printLogo() {
|
|
31
|
+
console.log(chalk.cyan.bold(`
|
|
32
|
+
โโโ โโโ โโโโโโ โโโ โโโโโโ โโโโโโโ
|
|
33
|
+
โโโ โโโโโโโโโโโโโโโ โโโโโโ โโโโโโโโ
|
|
34
|
+
โโโโโโโ โโโโโโโโโโโ โโโโโโ โโโโโโโโ
|
|
35
|
+
โโโโโโโ โโโโโโโโโโโโ โโโโโโโ โโโโโโโโ
|
|
36
|
+
โโโ โโโโโโ โโโ โโโโโโโ โโโโโโโโโโโ โโโ
|
|
37
|
+
โโโ โโโโโโ โโโ โโโโโ โโโโโโโโโโโ โโโ
|
|
38
|
+
`) + chalk.gray(` The AI Agent Action Gate v1.0.3
|
|
39
|
+
`));
|
|
62
40
|
}
|
|
63
41
|
|
|
64
|
-
async function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const response = await axios.post(API_URL, {
|
|
78
|
-
agent_id: isHook ? 'shell-hook' : 'cli-user',
|
|
79
|
-
action_type: cmd || 'run',
|
|
80
|
-
payload: payload
|
|
81
|
-
}, {
|
|
82
|
-
headers: { 'Authorization': 'Bearer ' + API_KEY }
|
|
42
|
+
async function cmdSetup() {
|
|
43
|
+
printLogo();
|
|
44
|
+
const readline = require('readline').createInterface({
|
|
45
|
+
input: process.stdin,
|
|
46
|
+
output: process.stdout
|
|
47
|
+
});
|
|
48
|
+
readline.question(chalk.cyan('Enter your KAVLR API Key (kavlr_...): '), (key) => {
|
|
49
|
+
if (key.startsWith('kavlr_')) {
|
|
50
|
+
saveKey(key.trim());
|
|
51
|
+
} else {
|
|
52
|
+
console.log(chalk.red('Invalid key format. It must start with "kavlr_"'));
|
|
53
|
+
}
|
|
54
|
+
readline.close();
|
|
83
55
|
});
|
|
56
|
+
}
|
|
84
57
|
|
|
85
|
-
|
|
86
|
-
|
|
58
|
+
async function cmdStatus() {
|
|
59
|
+
const key = loadKey();
|
|
60
|
+
console.log(chalk.gray('Fetching system status...'));
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`${SUPABASE_URL}/rest/v1/settings?id=eq.1&select=kill_switch_active`, {
|
|
63
|
+
headers: { 'apikey': SUPABASE_ANON, 'Authorization': `Bearer ${SUPABASE_ANON}` }
|
|
64
|
+
});
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
const isKilled = data[0] ? data[0].kill_switch_active : false;
|
|
67
|
+
|
|
68
|
+
console.log(chalk.bold('
|
|
69
|
+
SYSTEM STATUS'));
|
|
70
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
71
|
+
console.log(`API Key: ${chalk.green('CONFIGURED')}`);
|
|
72
|
+
console.log(`Kill Switch: ${isKilled ? chalk.red.bold('HALTED (LOCKDOWN)') : chalk.green.bold('SECURE (ACTIVE)')}`);
|
|
73
|
+
console.log(`Gate: ${chalk.cyan('ONLINE')}`);
|
|
74
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
75
|
+
');
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.log(chalk.red(`Error: ${e.message}`));
|
|
87
78
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function cmdLogs() {
|
|
82
|
+
const key = loadKey();
|
|
83
|
+
console.log(chalk.gray('Fetching recent telemetry...'));
|
|
84
|
+
try {
|
|
85
|
+
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/api_keys?key=eq.${key}&select=user_id`, {
|
|
86
|
+
headers: { 'apikey': SUPABASE_ANON, 'Authorization': `Bearer ${SUPABASE_ANON}` }
|
|
87
|
+
});
|
|
88
|
+
const users = await userRes.json();
|
|
89
|
+
|
|
90
|
+
const logsRes = await fetch(`${SUPABASE_URL}/rest/v1/agent_logs?user_id=eq.${users[0].user_id}&order=created_at.desc&limit=5`, {
|
|
91
|
+
headers: { 'apikey': SUPABASE_ANON, 'Authorization': `Bearer ${SUPABASE_ANON}` }
|
|
92
|
+
});
|
|
93
|
+
const logs = await logsRes.json();
|
|
94
|
+
|
|
95
|
+
console.log(chalk.bold('
|
|
96
|
+
RECENT TELEMETRY'));
|
|
97
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
98
|
+
logs.forEach(log => {
|
|
99
|
+
const color = log.status === 'BLOCKED' ? chalk.red : chalk.green;
|
|
100
|
+
console.log(`${color(`[${log.status}]`)} ${log.payload}`);
|
|
101
|
+
console.log(chalk.gray(` ID: ${log.agent_id} | Time: ${log.created_at.substring(11, 19)}`));
|
|
102
|
+
});
|
|
103
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
104
|
+
');
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.log(chalk.red(`Error: ${e.message}`));
|
|
95
107
|
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cmdDashboard() {
|
|
111
|
+
const url = "https://kavlr.netlify.app";
|
|
112
|
+
console.log(chalk.green(`Opening ${url}...`));
|
|
113
|
+
const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
114
|
+
execSync(`${start} ${url}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function runGate(args) {
|
|
118
|
+
const key = loadKey();
|
|
119
|
+
|
|
120
|
+
const binary = args[0];
|
|
121
|
+
const cmdArgs = args.slice(1);
|
|
96
122
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
console.log(chalk.gray(`[KAVLR GATE] Parsing action: ${chalk.bold(binary)} with args [${cmdArgs.join(', ')}]`));
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/evaluate_kavlr_action_v2`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'apikey': SUPABASE_ANON, 'Authorization': `Bearer ${SUPABASE_ANON}`, 'Content-Type': 'application/json' },
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
p_api_key: key, p_binary: binary, p_args: cmdArgs, p_cwd: process.cwd()
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
const verdict = await res.json();
|
|
134
|
+
|
|
135
|
+
if (verdict.reason === 'NEEDS_AI') {
|
|
136
|
+
console.log(chalk.yellow('[KAVLR AI] Unknown command. Requesting Constitutional AI review...'));
|
|
137
|
+
const aiRes = await fetch(`${SUPABASE_URL}/functions/v1/llm-judge`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({ binary: binary, args: cmdArgs })
|
|
141
|
+
});
|
|
142
|
+
const aiData = await aiRes.json();
|
|
143
|
+
|
|
144
|
+
if (aiData.score >= 90) {
|
|
145
|
+
console.log(chalk.green.bold(`โ
[ALLOWED] AI Score: ${aiData.score}/100`));
|
|
146
|
+
} else {
|
|
147
|
+
console.log(chalk.red.bold(`๐ [BLOCKED] AI Score: ${aiData.score}/100`));
|
|
148
|
+
console.log(chalk.red(` Reason: ${aiData.reason}`));
|
|
149
|
+
process.exit(403);
|
|
150
|
+
}
|
|
151
|
+
console.log(chalk.red.bold(`๐ [BLOCKED] ${verdict.reason}`));
|
|
152
|
+
process.exit(403);
|
|
153
|
+
} else {
|
|
154
|
+
console.log(chalk.green.bold(`โ
[ALLOWED] ${verdict.reason}`));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(chalk.gray('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
158
|
+
const child = spawn(binary, cmdArgs, { stdio: 'inherit' });
|
|
159
|
+
child.on('exit', (code) => {
|
|
160
|
+
console.log(chalk.gray('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
161
|
+
process.exit(code);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.log(chalk.red(`[KAVLR ERROR] ${e.message}`));
|
|
166
|
+
process.exit(1);
|
|
103
167
|
}
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
168
|
}
|
|
169
|
+
|
|
170
|
+
program
|
|
171
|
+
.name('kavlr')
|
|
172
|
+
.description('The AI Agent Action Gate')
|
|
173
|
+
.version('1.0.3')
|
|
174
|
+
.allowExcessArguments(true)
|
|
175
|
+
.allowUnknownOption(true)
|
|
176
|
+
.argument('[args...]', 'Command and arguments')
|
|
177
|
+
.action(async (args) => {
|
|
178
|
+
if (args.length === 0) {
|
|
179
|
+
printLogo();
|
|
180
|
+
console.log(`Usage:
|
|
181
|
+
kavlr setup
|
|
182
|
+
kavlr status
|
|
183
|
+
kavlr logs
|
|
184
|
+
kavlr dashboard
|
|
185
|
+
kavlr <command> [args...]`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const cmd = args[0];
|
|
190
|
+
if (cmd === 'setup') return cmdSetup();
|
|
191
|
+
if (cmd === 'status') return cmdStatus();
|
|
192
|
+
if (cmd === 'logs') return cmdLogs();
|
|
193
|
+
if (cmd === 'dashboard') return cmdDashboard();
|
|
194
|
+
|
|
195
|
+
await runGate(args);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hynaweb/kavlr",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "The Agent
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "The AI Agent Action Gate.",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kavlr": "bin/cli.js"
|
|
8
8
|
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"start": "node bin/cli.js"
|
|
11
|
-
},
|
|
12
9
|
"dependencies": {
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
10
|
+
"chalk": "^4.1.2",
|
|
11
|
+
"commander": "^11.1.0"
|
|
12
|
+
}
|
|
13
|
+
}
|