@xshieldai/agent-kernel 2.0.2
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/LICENSE +31 -0
- package/README.md +130 -0
- package/bin/kavachos +5 -0
- package/dist/apply-seccomp.py +625 -0
- package/dist/cgroup-egress.py +432 -0
- package/dist/kavachos.js +2687 -0
- package/package.json +52 -0
package/dist/kavachos.js
ADDED
|
@@ -0,0 +1,2687 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
8
|
+
var __toCommonJS = (from) => {
|
|
9
|
+
var entry = __moduleCache.get(from), desc;
|
|
10
|
+
if (entry)
|
|
11
|
+
return entry;
|
|
12
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
14
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
15
|
+
get: () => from[key],
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
}));
|
|
18
|
+
__moduleCache.set(from, entry);
|
|
19
|
+
return entry;
|
|
20
|
+
};
|
|
21
|
+
var __export = (target, all) => {
|
|
22
|
+
for (var name in all)
|
|
23
|
+
__defProp(target, name, {
|
|
24
|
+
get: all[name],
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
set: (newValue) => all[name] = () => newValue
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
31
|
+
var __require = import.meta.require;
|
|
32
|
+
|
|
33
|
+
// ../../src/kernel/syscall-profiles.ts
|
|
34
|
+
function buildSyscallSet(trustMask, domain) {
|
|
35
|
+
if (trustMask === 0)
|
|
36
|
+
return [...new Set(MINIMAL_READONLY_SYSCALLS)];
|
|
37
|
+
const set = new Set(BASELINE_SYSCALLS);
|
|
38
|
+
for (let bit = 0;bit < 16; bit++) {
|
|
39
|
+
if (trustMask & 1 << bit) {
|
|
40
|
+
const extras = TRUST_MASK_EXTRA_SYSCALLS[bit] ?? [];
|
|
41
|
+
extras.forEach((s) => set.add(s));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const domainExtras = DOMAIN_EXTRA_SYSCALLS[domain] ?? DOMAIN_EXTRA_SYSCALLS.general;
|
|
45
|
+
domainExtras.forEach((s) => set.add(s));
|
|
46
|
+
return [...set].sort();
|
|
47
|
+
}
|
|
48
|
+
var BASELINE_SYSCALLS, TRUST_MASK_EXTRA_SYSCALLS, DOMAIN_EXTRA_SYSCALLS, MINIMAL_READONLY_SYSCALLS, NOTIFY_SYSCALLS;
|
|
49
|
+
var init_syscall_profiles = __esm(() => {
|
|
50
|
+
BASELINE_SYSCALLS = [
|
|
51
|
+
"read",
|
|
52
|
+
"write",
|
|
53
|
+
"open",
|
|
54
|
+
"openat",
|
|
55
|
+
"close",
|
|
56
|
+
"stat",
|
|
57
|
+
"fstat",
|
|
58
|
+
"lstat",
|
|
59
|
+
"newfstatat",
|
|
60
|
+
"mmap",
|
|
61
|
+
"munmap",
|
|
62
|
+
"mprotect",
|
|
63
|
+
"brk",
|
|
64
|
+
"exit_group",
|
|
65
|
+
"exit",
|
|
66
|
+
"futex",
|
|
67
|
+
"rt_sigreturn",
|
|
68
|
+
"restart_syscall",
|
|
69
|
+
"nanosleep",
|
|
70
|
+
"getpid",
|
|
71
|
+
"gettid",
|
|
72
|
+
"getcwd",
|
|
73
|
+
"getdents64",
|
|
74
|
+
"socket",
|
|
75
|
+
"connect",
|
|
76
|
+
"sendto",
|
|
77
|
+
"recvfrom",
|
|
78
|
+
"execve",
|
|
79
|
+
"wait4",
|
|
80
|
+
"clone",
|
|
81
|
+
"clone3",
|
|
82
|
+
"fork",
|
|
83
|
+
"ioctl",
|
|
84
|
+
"fcntl",
|
|
85
|
+
"dup",
|
|
86
|
+
"dup2",
|
|
87
|
+
"dup3",
|
|
88
|
+
"pipe",
|
|
89
|
+
"pipe2",
|
|
90
|
+
"select",
|
|
91
|
+
"pselect6",
|
|
92
|
+
"poll",
|
|
93
|
+
"ppoll",
|
|
94
|
+
"epoll_create1",
|
|
95
|
+
"epoll_ctl",
|
|
96
|
+
"epoll_wait",
|
|
97
|
+
"epoll_pwait",
|
|
98
|
+
"gettimeofday",
|
|
99
|
+
"clock_gettime",
|
|
100
|
+
"clock_nanosleep",
|
|
101
|
+
"sigaltstack",
|
|
102
|
+
"rt_sigaction",
|
|
103
|
+
"rt_sigprocmask",
|
|
104
|
+
"getrusage",
|
|
105
|
+
"sysinfo",
|
|
106
|
+
"times",
|
|
107
|
+
"madvise",
|
|
108
|
+
"mincore",
|
|
109
|
+
"msync",
|
|
110
|
+
"getuid",
|
|
111
|
+
"getgid",
|
|
112
|
+
"geteuid",
|
|
113
|
+
"getegid",
|
|
114
|
+
"setuid",
|
|
115
|
+
"setgid",
|
|
116
|
+
"readlink",
|
|
117
|
+
"readlinkat",
|
|
118
|
+
"access",
|
|
119
|
+
"faccessat",
|
|
120
|
+
"faccessat2",
|
|
121
|
+
"uname",
|
|
122
|
+
"sethostname",
|
|
123
|
+
"pread64",
|
|
124
|
+
"pwrite64",
|
|
125
|
+
"readv",
|
|
126
|
+
"writev",
|
|
127
|
+
"preadv",
|
|
128
|
+
"pwritev",
|
|
129
|
+
"lseek",
|
|
130
|
+
"ftruncate",
|
|
131
|
+
"truncate",
|
|
132
|
+
"mkdir",
|
|
133
|
+
"mkdirat",
|
|
134
|
+
"rmdir",
|
|
135
|
+
"rename",
|
|
136
|
+
"renameat",
|
|
137
|
+
"renameat2",
|
|
138
|
+
"unlink",
|
|
139
|
+
"unlinkat",
|
|
140
|
+
"chmod",
|
|
141
|
+
"fchmod",
|
|
142
|
+
"fchmodat",
|
|
143
|
+
"chown",
|
|
144
|
+
"fchown",
|
|
145
|
+
"lchown",
|
|
146
|
+
"fchownat",
|
|
147
|
+
"sendmsg",
|
|
148
|
+
"recvmsg",
|
|
149
|
+
"sendmmsg",
|
|
150
|
+
"recvmmsg",
|
|
151
|
+
"getsockname",
|
|
152
|
+
"getpeername",
|
|
153
|
+
"getsockopt",
|
|
154
|
+
"setsockopt",
|
|
155
|
+
"bind",
|
|
156
|
+
"listen",
|
|
157
|
+
"accept",
|
|
158
|
+
"accept4",
|
|
159
|
+
"shutdown",
|
|
160
|
+
"statfs",
|
|
161
|
+
"fstatfs",
|
|
162
|
+
"utime",
|
|
163
|
+
"utimes",
|
|
164
|
+
"utimensat",
|
|
165
|
+
"futimesat",
|
|
166
|
+
"getrandom",
|
|
167
|
+
"sched_yield",
|
|
168
|
+
"sched_getaffinity",
|
|
169
|
+
"sched_setaffinity",
|
|
170
|
+
"prctl",
|
|
171
|
+
"arch_prctl",
|
|
172
|
+
"set_tid_address",
|
|
173
|
+
"set_robust_list",
|
|
174
|
+
"get_robust_list",
|
|
175
|
+
"seccomp",
|
|
176
|
+
"landlock_create_ruleset",
|
|
177
|
+
"landlock_add_rule",
|
|
178
|
+
"landlock_restrict_self"
|
|
179
|
+
];
|
|
180
|
+
TRUST_MASK_EXTRA_SYSCALLS = {
|
|
181
|
+
0: ["keyctl", "add_key", "request_key"],
|
|
182
|
+
1: ["capget", "capset"],
|
|
183
|
+
2: ["eventfd", "eventfd2", "signalfd", "signalfd4", "timerfd_create", "timerfd_settime", "timerfd_gettime"],
|
|
184
|
+
3: ["flock", "shmget", "shmat", "shmdt", "shmctl"],
|
|
185
|
+
4: ["sendfile", "splice", "tee"],
|
|
186
|
+
5: ["mlock", "munlock", "mlockall", "munlockall", "mremap"],
|
|
187
|
+
6: [],
|
|
188
|
+
7: ["memfd_create", "memfd_secret"],
|
|
189
|
+
8: ["mmap2"],
|
|
190
|
+
9: ["inotify_init1", "inotify_add_watch", "inotify_rm_watch", "inotify_init"],
|
|
191
|
+
10: [],
|
|
192
|
+
11: ["remap_file_pages", "mbind", "get_mempolicy", "set_mempolicy"],
|
|
193
|
+
12: [],
|
|
194
|
+
13: ["symlink", "symlinkat", "link", "linkat"],
|
|
195
|
+
14: ["kill", "tgkill", "tkill", "setpgid", "setsid", "getpgid", "getsid"],
|
|
196
|
+
15: ["execveat"]
|
|
197
|
+
};
|
|
198
|
+
DOMAIN_EXTRA_SYSCALLS = {
|
|
199
|
+
general: [],
|
|
200
|
+
maritime: [
|
|
201
|
+
"ioctl",
|
|
202
|
+
"tcsendbreak",
|
|
203
|
+
"tcdrain",
|
|
204
|
+
"tcflush",
|
|
205
|
+
"tcflow"
|
|
206
|
+
],
|
|
207
|
+
logistics: [],
|
|
208
|
+
ot: [
|
|
209
|
+
"ioctl",
|
|
210
|
+
"sched_setscheduler",
|
|
211
|
+
"sched_getscheduler",
|
|
212
|
+
"sched_setparam",
|
|
213
|
+
"sched_getparam"
|
|
214
|
+
],
|
|
215
|
+
finance: [
|
|
216
|
+
"ioctl"
|
|
217
|
+
]
|
|
218
|
+
};
|
|
219
|
+
MINIMAL_READONLY_SYSCALLS = [
|
|
220
|
+
"read",
|
|
221
|
+
"open",
|
|
222
|
+
"openat",
|
|
223
|
+
"close",
|
|
224
|
+
"stat",
|
|
225
|
+
"fstat",
|
|
226
|
+
"lstat",
|
|
227
|
+
"newfstatat",
|
|
228
|
+
"mmap",
|
|
229
|
+
"munmap",
|
|
230
|
+
"mprotect",
|
|
231
|
+
"brk",
|
|
232
|
+
"exit_group",
|
|
233
|
+
"exit",
|
|
234
|
+
"futex",
|
|
235
|
+
"rt_sigreturn",
|
|
236
|
+
"restart_syscall",
|
|
237
|
+
"getpid",
|
|
238
|
+
"gettid",
|
|
239
|
+
"getcwd",
|
|
240
|
+
"getdents64",
|
|
241
|
+
"readlink",
|
|
242
|
+
"readlinkat",
|
|
243
|
+
"access",
|
|
244
|
+
"faccessat",
|
|
245
|
+
"gettimeofday",
|
|
246
|
+
"clock_gettime",
|
|
247
|
+
"arch_prctl",
|
|
248
|
+
"set_tid_address",
|
|
249
|
+
"set_robust_list",
|
|
250
|
+
"rt_sigaction",
|
|
251
|
+
"rt_sigprocmask",
|
|
252
|
+
"clone3",
|
|
253
|
+
"write"
|
|
254
|
+
];
|
|
255
|
+
NOTIFY_SYSCALLS = [
|
|
256
|
+
"ptrace",
|
|
257
|
+
"bpf",
|
|
258
|
+
"mount",
|
|
259
|
+
"umount2",
|
|
260
|
+
"userfaultfd",
|
|
261
|
+
"perf_event_open",
|
|
262
|
+
"setns",
|
|
263
|
+
"capset"
|
|
264
|
+
];
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ../../src/kernel/seccomp-profile-generator.ts
|
|
268
|
+
var exports_seccomp_profile_generator = {};
|
|
269
|
+
__export(exports_seccomp_profile_generator, {
|
|
270
|
+
profileSummary: () => profileSummary,
|
|
271
|
+
generateSeccompProfile: () => generateSeccompProfile,
|
|
272
|
+
canonicalJson: () => canonicalJson
|
|
273
|
+
});
|
|
274
|
+
import { createHash } from "crypto";
|
|
275
|
+
function generateSeccompProfile(trustMask, domain, agentType = "claude-code") {
|
|
276
|
+
const syscalls = buildSyscallSet(trustMask, domain);
|
|
277
|
+
const allowSet = new Set(syscalls);
|
|
278
|
+
const notifySyscalls = trustMask > 0 ? NOTIFY_SYSCALLS.filter((s) => !allowSet.has(s)) : [];
|
|
279
|
+
const kSeal = createHash("sha256").update(syscalls.sort().join(",")).digest("hex");
|
|
280
|
+
const syscallEntries = [
|
|
281
|
+
{ names: syscalls, action: "SCMP_ACT_ALLOW" }
|
|
282
|
+
];
|
|
283
|
+
if (notifySyscalls.length > 0) {
|
|
284
|
+
syscallEntries.push({ names: notifySyscalls, action: "SCMP_ACT_NOTIFY" });
|
|
285
|
+
}
|
|
286
|
+
const profile = {
|
|
287
|
+
defaultAction: "SCMP_ACT_ERRNO",
|
|
288
|
+
architectures: ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"],
|
|
289
|
+
syscalls: syscallEntries,
|
|
290
|
+
_kavachos: {
|
|
291
|
+
version: "1.0",
|
|
292
|
+
trust_mask: trustMask,
|
|
293
|
+
domain,
|
|
294
|
+
agent_type: agentType,
|
|
295
|
+
generated_at: new Date().toISOString(),
|
|
296
|
+
k_seal: kSeal,
|
|
297
|
+
rule_ref: "KOS-010",
|
|
298
|
+
notify_syscalls: notifySyscalls
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const canonical = canonicalJson(profile);
|
|
302
|
+
const hash = createHash("sha256").update(canonical).digest("hex");
|
|
303
|
+
return { profile, hash, syscall_count: syscalls.length };
|
|
304
|
+
}
|
|
305
|
+
function canonicalJson(obj) {
|
|
306
|
+
if (obj === null || typeof obj !== "object")
|
|
307
|
+
return JSON.stringify(obj);
|
|
308
|
+
if (Array.isArray(obj))
|
|
309
|
+
return `[${obj.map(canonicalJson).join(",")}]`;
|
|
310
|
+
const sorted = Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",");
|
|
311
|
+
return `{${sorted}}`;
|
|
312
|
+
}
|
|
313
|
+
function profileSummary(result) {
|
|
314
|
+
const { profile, hash, syscall_count } = result;
|
|
315
|
+
return [
|
|
316
|
+
`KavachOS Seccomp Profile`,
|
|
317
|
+
` trust_mask: 0x${profile._kavachos.trust_mask.toString(16).padStart(8, "0")}`,
|
|
318
|
+
` domain: ${profile._kavachos.domain}`,
|
|
319
|
+
` agent_type: ${profile._kavachos.agent_type}`,
|
|
320
|
+
` syscalls: ${syscall_count}`,
|
|
321
|
+
` notify: ${profile._kavachos.notify_syscalls.length} syscalls (supervisor gates)`,
|
|
322
|
+
` default: ERRNO (deny-all unmatched)`,
|
|
323
|
+
` k_seal: ${profile._kavachos.k_seal.slice(0, 16)}...`,
|
|
324
|
+
` profile_hash: ${hash.slice(0, 16)}...`,
|
|
325
|
+
` rule_ref: KOS-010`
|
|
326
|
+
].join(`
|
|
327
|
+
`);
|
|
328
|
+
}
|
|
329
|
+
var init_seccomp_profile_generator = __esm(() => {
|
|
330
|
+
init_syscall_profiles();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// ../../src/kernel/falco-rule-generator.ts
|
|
334
|
+
var exports_falco_rule_generator = {};
|
|
335
|
+
__export(exports_falco_rule_generator, {
|
|
336
|
+
generateFalcoRules: () => generateFalcoRules
|
|
337
|
+
});
|
|
338
|
+
function generateFalcoRules(domain, trustMask) {
|
|
339
|
+
const baseRules = generateBaseRules(trustMask);
|
|
340
|
+
const domainRules = generateDomainRules(domain, trustMask);
|
|
341
|
+
const allRules = [...baseRules, ...domainRules];
|
|
342
|
+
const yaml = buildYaml(allRules);
|
|
343
|
+
return {
|
|
344
|
+
version: "1.0",
|
|
345
|
+
domain,
|
|
346
|
+
trust_mask: trustMask,
|
|
347
|
+
rules: yaml,
|
|
348
|
+
rule_count: allRules.length,
|
|
349
|
+
generated_at: new Date().toISOString()
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function generateBaseRules(trustMask) {
|
|
353
|
+
const rules = [];
|
|
354
|
+
rules.push({
|
|
355
|
+
name: "kavachos_unexpected_execve",
|
|
356
|
+
desc: "AI agent executed a binary not in the declared tool scope (potential exfil or lateral movement)",
|
|
357
|
+
condition: `evt.type = execve and proc.name in (node, bun, claude) and not proc.args startswith "${ALLOWED_CLAUDE_BINARIES.slice(0, 8).join('" and not proc.args startswith "')}"`,
|
|
358
|
+
output: "KavachOS: unexpected execve by AI agent (agent=%proc.name cmd=%proc.cmdline user=%user.name pid=%proc.pid)",
|
|
359
|
+
priority: "WARNING",
|
|
360
|
+
tags: ["kavachos", "process", "kos-infer-004"]
|
|
361
|
+
});
|
|
362
|
+
rules.push({
|
|
363
|
+
name: "kavachos_exfil_command",
|
|
364
|
+
desc: "AI agent attempted to execute a known data exfiltration command",
|
|
365
|
+
condition: `evt.type = execve and (${EXFIL_COMMANDS.map((c) => `proc.cmdline contains "${c.split(" ")[0]}"`).join(" or ")})`,
|
|
366
|
+
output: "KavachOS: EXFIL PATTERN detected (cmd=%proc.cmdline user=%user.name pid=%proc.pid container.id=%container.id)",
|
|
367
|
+
priority: "CRITICAL",
|
|
368
|
+
tags: ["kavachos", "exfil", "kos-007"]
|
|
369
|
+
});
|
|
370
|
+
rules.push({
|
|
371
|
+
name: "kavachos_file_write_outside_scope",
|
|
372
|
+
desc: "AI agent wrote a file outside its declared working directory",
|
|
373
|
+
condition: "evt.type in (open, openat) and evt.arg.flags contains O_WRONLY and not fd.name startswith /tmp and not fd.name startswith /root/.claude and not fd.name startswith /root/",
|
|
374
|
+
output: "KavachOS: file write outside scope (file=%fd.name agent=%proc.name pid=%proc.pid)",
|
|
375
|
+
priority: "WARNING",
|
|
376
|
+
tags: ["kavachos", "file", "kos-003"]
|
|
377
|
+
});
|
|
378
|
+
rules.push({
|
|
379
|
+
name: "kavachos_credential_read",
|
|
380
|
+
desc: "AI agent read a credential or private key file",
|
|
381
|
+
condition: `evt.type in (open, openat) and (fd.name contains ".pem" or fd.name contains ".key" or fd.name contains "id_rsa" or fd.name contains "credentials" or fd.name contains ".env" or fd.name endswith ".p12" or fd.name endswith ".pfx")`,
|
|
382
|
+
output: "KavachOS: credential file access (file=%fd.name agent=%proc.name pid=%proc.pid)",
|
|
383
|
+
priority: "ERROR",
|
|
384
|
+
tags: ["kavachos", "credentials", "kos-007"]
|
|
385
|
+
});
|
|
386
|
+
if (trustMask === 0) {
|
|
387
|
+
rules.push({
|
|
388
|
+
name: "kavachos_readonly_agent_network",
|
|
389
|
+
desc: "Read-only agent (trust_mask=0) attempted network connection \u2014 violation of INF-KOS-001",
|
|
390
|
+
condition: "evt.type in (connect, sendto, sendmsg) and proc.env contains KAVACHOS_TRUST_MASK=0",
|
|
391
|
+
output: "KavachOS: read-only agent network attempt BLOCKED (agent=%proc.name dst=%fd.rip pid=%proc.pid)",
|
|
392
|
+
priority: "CRITICAL",
|
|
393
|
+
tags: ["kavachos", "trust-mask-zero", "inf-kos-001"]
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return rules;
|
|
397
|
+
}
|
|
398
|
+
function generateDomainRules(domain, trustMask) {
|
|
399
|
+
switch (domain) {
|
|
400
|
+
case "maritime":
|
|
401
|
+
return generateMaritimeRules(trustMask);
|
|
402
|
+
case "logistics":
|
|
403
|
+
return generateLogisticsRules(trustMask);
|
|
404
|
+
case "ot":
|
|
405
|
+
return generateOTRules(trustMask);
|
|
406
|
+
case "finance":
|
|
407
|
+
return generateFinanceRules(trustMask);
|
|
408
|
+
default:
|
|
409
|
+
return generateGeneralRules(trustMask);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function generateMaritimeRules(_trustMask) {
|
|
413
|
+
return [
|
|
414
|
+
{
|
|
415
|
+
name: "kavachos_maritime_nmea_write",
|
|
416
|
+
desc: "Agent wrote to NMEA serial device \u2014 potential navigation system tampering (MAR-001)",
|
|
417
|
+
condition: "evt.type in (open, openat) and evt.arg.flags contains O_WRONLY and (fd.name startswith /dev/ttyS or fd.name startswith /dev/ttyUSB)",
|
|
418
|
+
output: "KavachOS MARITIME: NMEA device write attempt (device=%fd.name agent=%proc.name pid=%proc.pid)",
|
|
419
|
+
priority: "CRITICAL",
|
|
420
|
+
tags: ["kavachos", "maritime", "nmea", "mar-001"]
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: "kavachos_maritime_ais_inject",
|
|
424
|
+
desc: "Agent attempted to write AIS position data \u2014 potential vessel spoofing (MAR-004)",
|
|
425
|
+
condition: `evt.type = write and fd.name contains "ais" and evt.arg.data startswith "!AIVDM"`,
|
|
426
|
+
output: "KavachOS MARITIME: AIS data injection attempt (agent=%proc.name pid=%proc.pid)",
|
|
427
|
+
priority: "CRITICAL",
|
|
428
|
+
tags: ["kavachos", "maritime", "ais", "mar-004"]
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: "kavachos_maritime_stcw_data",
|
|
432
|
+
desc: "Agent accessed STCW crew data file without maritime trust_mask bit set",
|
|
433
|
+
condition: `evt.type in (open, openat) and (fd.name contains "stcw" or fd.name contains "crew" or fd.name contains "certificate") and not proc.env contains "KAVACHOS_DOMAIN=maritime"`,
|
|
434
|
+
output: "KavachOS MARITIME: STCW data accessed without maritime clearance (file=%fd.name agent=%proc.name)",
|
|
435
|
+
priority: "ERROR",
|
|
436
|
+
tags: ["kavachos", "maritime", "stcw", "inf-kos-005"]
|
|
437
|
+
}
|
|
438
|
+
];
|
|
439
|
+
}
|
|
440
|
+
function generateLogisticsRules(_trustMask) {
|
|
441
|
+
return [
|
|
442
|
+
{
|
|
443
|
+
name: "kavachos_logistics_ebl_write",
|
|
444
|
+
desc: "Agent wrote to eBL document store without eBL trust bit \u2014 potential document fraud",
|
|
445
|
+
condition: `evt.type in (open, openat) and evt.arg.flags contains O_WRONLY and (fd.name contains "ebl" or fd.name contains "bill-of-lading") and not proc.env contains "KAVACHOS_EBL_AUTHORIZED=1"`,
|
|
446
|
+
output: "KavachOS LOGISTICS: unauthorized eBL write (file=%fd.name agent=%proc.name pid=%proc.pid)",
|
|
447
|
+
priority: "ERROR",
|
|
448
|
+
tags: ["kavachos", "logistics", "ebl"]
|
|
449
|
+
}
|
|
450
|
+
];
|
|
451
|
+
}
|
|
452
|
+
function generateOTRules(_trustMask) {
|
|
453
|
+
return [
|
|
454
|
+
{
|
|
455
|
+
name: "kavachos_ot_modbus_write",
|
|
456
|
+
desc: "Agent wrote to Modbus register \u2014 potential OT control system manipulation (IEC 62443)",
|
|
457
|
+
condition: `evt.type = sendto and fd.rport = 502 and evt.arg.data != ""`,
|
|
458
|
+
output: "KavachOS OT: Modbus write detected (dst=%fd.rip:%fd.rport agent=%proc.name pid=%proc.pid)",
|
|
459
|
+
priority: "CRITICAL",
|
|
460
|
+
tags: ["kavachos", "ot", "modbus", "iec-62443"]
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "kavachos_ot_alarm_suppress",
|
|
464
|
+
desc: "Agent suppressed an OT alarm signal \u2014 critical safety violation",
|
|
465
|
+
condition: `evt.type = write and (fd.name contains "alarm" or fd.name contains "safety") and evt.arg.data contains "suppress"`,
|
|
466
|
+
output: "KavachOS OT: ALARM SUPPRESS attempt (file=%fd.name agent=%proc.name pid=%proc.pid)",
|
|
467
|
+
priority: "CRITICAL",
|
|
468
|
+
tags: ["kavachos", "ot", "safety", "varuna"]
|
|
469
|
+
}
|
|
470
|
+
];
|
|
471
|
+
}
|
|
472
|
+
function generateFinanceRules(_trustMask) {
|
|
473
|
+
return [
|
|
474
|
+
{
|
|
475
|
+
name: "kavachos_fin_payment_write",
|
|
476
|
+
desc: "Agent wrote payment instruction without financial authorization trust bit",
|
|
477
|
+
condition: `evt.type = sendto and (fd.rport = 443 or fd.rport = 8443) and proc.env contains "PAYMENT_INSTRUCTION" and not proc.env contains "KAVACHOS_FIN_AUTHORIZED=1"`,
|
|
478
|
+
output: "KavachOS FIN: unauthorized payment instruction (agent=%proc.name dst=%fd.rip pid=%proc.pid)",
|
|
479
|
+
priority: "CRITICAL",
|
|
480
|
+
tags: ["kavachos", "finance", "payment", "kos-fin-class3"]
|
|
481
|
+
}
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
function generateGeneralRules(_trustMask) {
|
|
485
|
+
return [
|
|
486
|
+
{
|
|
487
|
+
name: "kavachos_general_shell_spawn",
|
|
488
|
+
desc: "Agent spawned an interactive shell \u2014 potential container escape vector",
|
|
489
|
+
condition: `evt.type = execve and (proc.name = "bash" or proc.name = "sh" or proc.name = "zsh") and proc.args contains "-i"`,
|
|
490
|
+
output: "KavachOS: interactive shell spawn by AI agent (agent=%proc.name cmd=%proc.cmdline pid=%proc.pid)",
|
|
491
|
+
priority: "WARNING",
|
|
492
|
+
tags: ["kavachos", "general", "shell"]
|
|
493
|
+
}
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
function buildYaml(rules) {
|
|
497
|
+
return rules.map((r) => `
|
|
498
|
+
- rule: ${r.name}
|
|
499
|
+
desc: ${r.desc}
|
|
500
|
+
condition: >
|
|
501
|
+
${r.condition}
|
|
502
|
+
output: "${r.output}"
|
|
503
|
+
priority: ${r.priority}
|
|
504
|
+
tags: [${r.tags.join(", ")}]
|
|
505
|
+
`).join(`
|
|
506
|
+
`);
|
|
507
|
+
}
|
|
508
|
+
var EXFIL_COMMANDS, ALLOWED_CLAUDE_BINARIES;
|
|
509
|
+
var init_falco_rule_generator = __esm(() => {
|
|
510
|
+
EXFIL_COMMANDS = [
|
|
511
|
+
"curl",
|
|
512
|
+
"wget",
|
|
513
|
+
"nc",
|
|
514
|
+
"ncat",
|
|
515
|
+
"netcat",
|
|
516
|
+
"python3 -c",
|
|
517
|
+
"python -c",
|
|
518
|
+
"bash -i",
|
|
519
|
+
"sh -i",
|
|
520
|
+
"socat",
|
|
521
|
+
"openssl s_client"
|
|
522
|
+
];
|
|
523
|
+
ALLOWED_CLAUDE_BINARIES = [
|
|
524
|
+
"node",
|
|
525
|
+
"bun",
|
|
526
|
+
"claude",
|
|
527
|
+
"git",
|
|
528
|
+
"npm",
|
|
529
|
+
"npx",
|
|
530
|
+
"cat",
|
|
531
|
+
"grep",
|
|
532
|
+
"ls",
|
|
533
|
+
"find",
|
|
534
|
+
"sed",
|
|
535
|
+
"awk",
|
|
536
|
+
"head",
|
|
537
|
+
"tail",
|
|
538
|
+
"wc",
|
|
539
|
+
"sort",
|
|
540
|
+
"uniq",
|
|
541
|
+
"mkdir",
|
|
542
|
+
"cp",
|
|
543
|
+
"mv",
|
|
544
|
+
"rm",
|
|
545
|
+
"touch",
|
|
546
|
+
"tar",
|
|
547
|
+
"gzip",
|
|
548
|
+
"gunzip",
|
|
549
|
+
"psql",
|
|
550
|
+
"pg_dump",
|
|
551
|
+
"docker",
|
|
552
|
+
"python3",
|
|
553
|
+
"python",
|
|
554
|
+
"bash",
|
|
555
|
+
"sh",
|
|
556
|
+
"zsh"
|
|
557
|
+
];
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// ../../src/core/config.ts
|
|
561
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
562
|
+
import { join } from "path";
|
|
563
|
+
function getAegisDir() {
|
|
564
|
+
return AEGIS_DIR;
|
|
565
|
+
}
|
|
566
|
+
function getDbPath() {
|
|
567
|
+
return join(AEGIS_DIR, "aegis.db");
|
|
568
|
+
}
|
|
569
|
+
function ensureAegisDir() {
|
|
570
|
+
if (!existsSync(AEGIS_DIR)) {
|
|
571
|
+
mkdirSync(AEGIS_DIR, { recursive: true });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function loadConfig() {
|
|
575
|
+
ensureAegisDir();
|
|
576
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
577
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
578
|
+
return DEFAULT_CONFIG;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
582
|
+
const parsed = JSON.parse(raw);
|
|
583
|
+
return { ...DEFAULT_CONFIG, ...parsed, budget: { ...DEFAULT_CONFIG.budget, ...parsed.budget } };
|
|
584
|
+
} catch {
|
|
585
|
+
return DEFAULT_CONFIG;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
var AEGIS_DIR, CONFIG_PATH, DEFAULT_CONFIG;
|
|
589
|
+
var init_config = __esm(() => {
|
|
590
|
+
AEGIS_DIR = join(process.env.HOME || "/root", ".aegis");
|
|
591
|
+
CONFIG_PATH = join(AEGIS_DIR, "config.json");
|
|
592
|
+
DEFAULT_CONFIG = {
|
|
593
|
+
plan: "max_5x",
|
|
594
|
+
budget: {
|
|
595
|
+
daily_limit_usd: 100,
|
|
596
|
+
weekly_limit_usd: 400,
|
|
597
|
+
monthly_limit_usd: 1200,
|
|
598
|
+
session_limit_usd: 25,
|
|
599
|
+
messages_per_5h: 225,
|
|
600
|
+
tokens_per_5h: 50000000,
|
|
601
|
+
weekly_messages: 3150,
|
|
602
|
+
weekly_tokens: 700000000,
|
|
603
|
+
spawn_limit_per_session: 50,
|
|
604
|
+
spawn_concurrent_max: 20,
|
|
605
|
+
cost_estimate_threshold_usd: 10,
|
|
606
|
+
max_depth: 5
|
|
607
|
+
},
|
|
608
|
+
heartbeat: {
|
|
609
|
+
timeout_seconds: 300,
|
|
610
|
+
action: "alert"
|
|
611
|
+
},
|
|
612
|
+
pricing_mode: "api",
|
|
613
|
+
max_plan_discount: 0.2,
|
|
614
|
+
dashboard: {
|
|
615
|
+
port: 4850,
|
|
616
|
+
auth: {
|
|
617
|
+
enabled: false,
|
|
618
|
+
username: "aegis",
|
|
619
|
+
password: "changeme"
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
monitor: {
|
|
623
|
+
health_port: 4851,
|
|
624
|
+
watch_paths: ["~/.claude/projects"],
|
|
625
|
+
poll_interval_ms: 2000
|
|
626
|
+
},
|
|
627
|
+
alerts: {
|
|
628
|
+
terminal_bell: true,
|
|
629
|
+
webhook_url: null
|
|
630
|
+
},
|
|
631
|
+
kavach: {
|
|
632
|
+
enabled: true,
|
|
633
|
+
notify_channel: "telegram",
|
|
634
|
+
notify_telegram_chat_id: process.env.KAVACH_TG_CHAT_ID || "",
|
|
635
|
+
notify_phone: process.env.KAVACH_NOTIFY_PHONE || "",
|
|
636
|
+
notify_email: process.env.KAVACH_NOTIFY_EMAIL || "",
|
|
637
|
+
notify_via_webhook: false,
|
|
638
|
+
webhook_url: "",
|
|
639
|
+
timeout_level1_s: 600,
|
|
640
|
+
timeout_level2_s: 300,
|
|
641
|
+
timeout_level3_s: 120,
|
|
642
|
+
timeout_level4_s: 60
|
|
643
|
+
},
|
|
644
|
+
enforcement: {
|
|
645
|
+
mode: "alert",
|
|
646
|
+
excluded_pids: [],
|
|
647
|
+
excluded_ppids: [],
|
|
648
|
+
registry_url: null,
|
|
649
|
+
registry_admin_key: null,
|
|
650
|
+
auto_restart_services: [],
|
|
651
|
+
auto_restart_delay_ms: 3000
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// ../../src/core/db.ts
|
|
657
|
+
var exports_db = {};
|
|
658
|
+
__export(exports_db, {
|
|
659
|
+
upsertSession: () => upsertSession,
|
|
660
|
+
upsertAgent: () => upsertAgent,
|
|
661
|
+
touchAgent: () => touchAgent,
|
|
662
|
+
setSessionStatus: () => setSessionStatus,
|
|
663
|
+
setAgentState: () => setAgentState,
|
|
664
|
+
requestStop: () => requestStop,
|
|
665
|
+
recordDashboardAccess: () => recordDashboardAccess,
|
|
666
|
+
recordAgentUsage: () => recordAgentUsage,
|
|
667
|
+
rebalanceBudget: () => rebalanceBudget,
|
|
668
|
+
markKavachNotified: () => markKavachNotified,
|
|
669
|
+
listAgentRows: () => listAgentRows,
|
|
670
|
+
listActiveSessions: () => listActiveSessions,
|
|
671
|
+
isStopRequested: () => isStopRequested,
|
|
672
|
+
incrementViolationCount: () => incrementViolationCount,
|
|
673
|
+
getWindowBudget: () => getWindowBudget,
|
|
674
|
+
getSessionSpawnCount: () => getSessionSpawnCount,
|
|
675
|
+
getSession: () => getSession,
|
|
676
|
+
getRecentApprovals: () => getRecentApprovals,
|
|
677
|
+
getRecentAlerts: () => getRecentAlerts,
|
|
678
|
+
getPendingApprovals: () => getPendingApprovals,
|
|
679
|
+
getKavachApproval: () => getKavachApproval,
|
|
680
|
+
getDistinctDashboardIpCount: () => getDistinctDashboardIpCount,
|
|
681
|
+
getDb: () => getDb,
|
|
682
|
+
getDailySpend: () => getDailySpend,
|
|
683
|
+
getCostTree: () => getCostTree,
|
|
684
|
+
getBudgetState: () => getBudgetState,
|
|
685
|
+
getAgentRow: () => getAgentRow,
|
|
686
|
+
getAgentCostProjection: () => getAgentCostProjection,
|
|
687
|
+
decideKavachApproval: () => decideKavachApproval,
|
|
688
|
+
createKavachApproval: () => createKavachApproval,
|
|
689
|
+
checkBudgetInheritance: () => checkBudgetInheritance,
|
|
690
|
+
addUsage: () => addUsage,
|
|
691
|
+
addToBudget: () => addToBudget,
|
|
692
|
+
addAlert: () => addAlert
|
|
693
|
+
});
|
|
694
|
+
import { Database } from "bun:sqlite";
|
|
695
|
+
function getDb() {
|
|
696
|
+
if (_db)
|
|
697
|
+
return _db;
|
|
698
|
+
ensureAegisDir();
|
|
699
|
+
_db = new Database(getDbPath(), { create: true });
|
|
700
|
+
_db.exec("PRAGMA journal_mode = WAL");
|
|
701
|
+
_db.exec("PRAGMA busy_timeout = 5000");
|
|
702
|
+
initSchema(_db);
|
|
703
|
+
return _db;
|
|
704
|
+
}
|
|
705
|
+
function initSchema(db) {
|
|
706
|
+
db.exec(`
|
|
707
|
+
CREATE TABLE IF NOT EXISTS usage_log (
|
|
708
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
709
|
+
session_id TEXT NOT NULL,
|
|
710
|
+
timestamp TEXT NOT NULL,
|
|
711
|
+
model TEXT,
|
|
712
|
+
input_tokens INTEGER DEFAULT 0,
|
|
713
|
+
output_tokens INTEGER DEFAULT 0,
|
|
714
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
715
|
+
cache_creation_tokens INTEGER DEFAULT 0,
|
|
716
|
+
estimated_cost_usd REAL DEFAULT 0,
|
|
717
|
+
is_agent_spawn INTEGER DEFAULT 0,
|
|
718
|
+
raw_json TEXT
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
722
|
+
session_id TEXT PRIMARY KEY,
|
|
723
|
+
project_path TEXT,
|
|
724
|
+
first_seen TEXT NOT NULL,
|
|
725
|
+
last_activity TEXT NOT NULL,
|
|
726
|
+
total_cost_usd REAL DEFAULT 0,
|
|
727
|
+
message_count INTEGER DEFAULT 0,
|
|
728
|
+
agent_spawns INTEGER DEFAULT 0,
|
|
729
|
+
status TEXT DEFAULT 'active'
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
CREATE TABLE IF NOT EXISTS budget_state (
|
|
733
|
+
period TEXT PRIMARY KEY,
|
|
734
|
+
spent_usd REAL DEFAULT 0,
|
|
735
|
+
limit_usd REAL,
|
|
736
|
+
last_updated TEXT
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
CREATE TABLE IF NOT EXISTS alerts (
|
|
740
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
741
|
+
type TEXT NOT NULL,
|
|
742
|
+
severity TEXT NOT NULL,
|
|
743
|
+
message TEXT,
|
|
744
|
+
session_id TEXT,
|
|
745
|
+
timestamp TEXT NOT NULL,
|
|
746
|
+
acknowledged INTEGER DEFAULT 0
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
CREATE INDEX IF NOT EXISTS idx_usage_session ON usage_log(session_id);
|
|
750
|
+
CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_log(timestamp);
|
|
751
|
+
CREATE INDEX IF NOT EXISTS idx_alerts_timestamp ON alerts(timestamp);
|
|
752
|
+
|
|
753
|
+
CREATE TABLE IF NOT EXISTS kavach_approvals (
|
|
754
|
+
id TEXT PRIMARY KEY,
|
|
755
|
+
created_at TEXT NOT NULL,
|
|
756
|
+
command TEXT NOT NULL,
|
|
757
|
+
tool_name TEXT NOT NULL DEFAULT 'Bash',
|
|
758
|
+
level INTEGER NOT NULL,
|
|
759
|
+
consequence TEXT NOT NULL,
|
|
760
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
761
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
762
|
+
first_approver TEXT,
|
|
763
|
+
decided_at TEXT,
|
|
764
|
+
decided_by TEXT,
|
|
765
|
+
notified INTEGER DEFAULT 0,
|
|
766
|
+
timeout_ms INTEGER NOT NULL DEFAULT 600000
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
-- Migration: add first_approver if upgrading from older schema
|
|
770
|
+
CREATE INDEX IF NOT EXISTS idx_kavach_status ON kavach_approvals(status);
|
|
771
|
+
CREATE INDEX IF NOT EXISTS idx_kavach_created ON kavach_approvals(created_at);
|
|
772
|
+
|
|
773
|
+
-- Phase 2: durable agent session registry (V2-040)
|
|
774
|
+
-- @rule:KAV-002 Agent check-in, KAV-008 quarantine survives restart, KAV-011 identity
|
|
775
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
776
|
+
agent_id TEXT PRIMARY KEY,
|
|
777
|
+
state TEXT NOT NULL DEFAULT 'RUNNING',
|
|
778
|
+
identity_confidence TEXT NOT NULL DEFAULT 'unknown',
|
|
779
|
+
parent_id TEXT,
|
|
780
|
+
session_id TEXT NOT NULL,
|
|
781
|
+
depth INTEGER DEFAULT 0,
|
|
782
|
+
budget_cap_usd REAL DEFAULT 0,
|
|
783
|
+
budget_used_usd REAL DEFAULT 0,
|
|
784
|
+
budget_pool_reserved REAL DEFAULT 0,
|
|
785
|
+
tool_calls INTEGER DEFAULT 0,
|
|
786
|
+
loop_count INTEGER DEFAULT 0,
|
|
787
|
+
tools_declared INTEGER DEFAULT 0,
|
|
788
|
+
violation_count INTEGER DEFAULT 0,
|
|
789
|
+
spawn_timestamp TEXT NOT NULL,
|
|
790
|
+
last_seen TEXT NOT NULL,
|
|
791
|
+
policy_path TEXT,
|
|
792
|
+
stop_requested INTEGER DEFAULT 0,
|
|
793
|
+
quarantine_reason TEXT,
|
|
794
|
+
quarantine_rule TEXT,
|
|
795
|
+
release_reason TEXT,
|
|
796
|
+
released_by TEXT,
|
|
797
|
+
resume_manifest_path TEXT
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
CREATE INDEX IF NOT EXISTS idx_agents_state ON agents(state);
|
|
801
|
+
CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
|
|
802
|
+
CREATE INDEX IF NOT EXISTS idx_agents_last_seen ON agents(last_seen);
|
|
803
|
+
|
|
804
|
+
-- @rule:KAV-066 \u2014 hosted-service detection (V2-101)
|
|
805
|
+
-- Logs each unique IP that accesses the dashboard; watchdog checks distinct IP count
|
|
806
|
+
CREATE TABLE IF NOT EXISTS dashboard_access (
|
|
807
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
808
|
+
ip TEXT NOT NULL,
|
|
809
|
+
timestamp TEXT NOT NULL,
|
|
810
|
+
path TEXT
|
|
811
|
+
);
|
|
812
|
+
CREATE INDEX IF NOT EXISTS idx_dashboard_access_ip ON dashboard_access(ip);
|
|
813
|
+
CREATE INDEX IF NOT EXISTS idx_dashboard_access_ts ON dashboard_access(timestamp);
|
|
814
|
+
`);
|
|
815
|
+
try {
|
|
816
|
+
db.exec(`ALTER TABLE kavach_approvals ADD COLUMN first_approver TEXT`);
|
|
817
|
+
} catch {}
|
|
818
|
+
try {
|
|
819
|
+
db.exec(`ALTER TABLE agents ADD COLUMN loop_count INTEGER DEFAULT 0`);
|
|
820
|
+
} catch {}
|
|
821
|
+
try {
|
|
822
|
+
db.exec(`ALTER TABLE agents ADD COLUMN tools_declared INTEGER DEFAULT 0`);
|
|
823
|
+
} catch {}
|
|
824
|
+
try {
|
|
825
|
+
db.exec(`ALTER TABLE agents ADD COLUMN stop_requested INTEGER DEFAULT 0`);
|
|
826
|
+
} catch {}
|
|
827
|
+
try {
|
|
828
|
+
db.exec(`ALTER TABLE agents ADD COLUMN budget_pool_reserved REAL DEFAULT 0`);
|
|
829
|
+
} catch {}
|
|
830
|
+
try {
|
|
831
|
+
db.exec(`ALTER TABLE agents ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
832
|
+
} catch {}
|
|
833
|
+
try {
|
|
834
|
+
db.exec(`ALTER TABLE kavach_approvals ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
835
|
+
} catch {}
|
|
836
|
+
try {
|
|
837
|
+
db.exec(`ALTER TABLE alerts ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
838
|
+
} catch {}
|
|
839
|
+
try {
|
|
840
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
841
|
+
} catch {}
|
|
842
|
+
try {
|
|
843
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_agents_tenant ON agents(tenant_id)`);
|
|
844
|
+
} catch {}
|
|
845
|
+
try {
|
|
846
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_approvals_tenant ON kavach_approvals(tenant_id)`);
|
|
847
|
+
} catch {}
|
|
848
|
+
try {
|
|
849
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_alerts_tenant ON alerts(tenant_id)`);
|
|
850
|
+
} catch {}
|
|
851
|
+
}
|
|
852
|
+
function recordDashboardAccess(ip, path) {
|
|
853
|
+
const db = getDb();
|
|
854
|
+
db.run("INSERT INTO dashboard_access (ip, timestamp, path) VALUES (?, ?, ?)", [ip, new Date().toISOString(), path]);
|
|
855
|
+
}
|
|
856
|
+
function getDistinctDashboardIpCount(sinceHours = 24 * 7) {
|
|
857
|
+
const db = getDb();
|
|
858
|
+
const since = new Date(Date.now() - sinceHours * 60 * 60 * 1000).toISOString();
|
|
859
|
+
const row = db.query("SELECT COUNT(DISTINCT ip) as cnt FROM dashboard_access WHERE timestamp >= ?").get(since);
|
|
860
|
+
return row?.cnt ?? 0;
|
|
861
|
+
}
|
|
862
|
+
function addUsage(record) {
|
|
863
|
+
const db = getDb();
|
|
864
|
+
db.run(`INSERT INTO usage_log (session_id, timestamp, model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, estimated_cost_usd, is_agent_spawn, raw_json)
|
|
865
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
866
|
+
record.session_id,
|
|
867
|
+
record.timestamp,
|
|
868
|
+
record.model,
|
|
869
|
+
record.input_tokens,
|
|
870
|
+
record.output_tokens,
|
|
871
|
+
record.cache_read_tokens,
|
|
872
|
+
record.cache_creation_tokens,
|
|
873
|
+
record.estimated_cost_usd,
|
|
874
|
+
record.is_agent_spawn ? 1 : 0,
|
|
875
|
+
record.raw_json || null
|
|
876
|
+
]);
|
|
877
|
+
}
|
|
878
|
+
function upsertSession(session_id, project_path, cost, is_spawn) {
|
|
879
|
+
const db = getDb();
|
|
880
|
+
const now = new Date().toISOString();
|
|
881
|
+
const existing = db.query("SELECT * FROM sessions WHERE session_id = ?").get(session_id);
|
|
882
|
+
if (existing) {
|
|
883
|
+
db.run(`UPDATE sessions SET last_activity = ?, total_cost_usd = total_cost_usd + ?, message_count = message_count + 1,
|
|
884
|
+
agent_spawns = agent_spawns + ? WHERE session_id = ?`, [now, cost, is_spawn ? 1 : 0, session_id]);
|
|
885
|
+
} else {
|
|
886
|
+
db.run(`INSERT INTO sessions (session_id, project_path, first_seen, last_activity, total_cost_usd, message_count, agent_spawns, status)
|
|
887
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, 'active')`, [session_id, project_path, now, now, cost, is_spawn ? 1 : 0]);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function listActiveSessions() {
|
|
891
|
+
const db = getDb();
|
|
892
|
+
return db.query("SELECT * FROM sessions WHERE status IN ('active', 'paused') ORDER BY last_activity DESC").all();
|
|
893
|
+
}
|
|
894
|
+
function getSession(session_id) {
|
|
895
|
+
const db = getDb();
|
|
896
|
+
return db.query("SELECT * FROM sessions WHERE session_id = ?").get(session_id);
|
|
897
|
+
}
|
|
898
|
+
function setSessionStatus(session_id, status) {
|
|
899
|
+
const db = getDb();
|
|
900
|
+
db.run("UPDATE sessions SET status = ? WHERE session_id = ?", [status, session_id]);
|
|
901
|
+
}
|
|
902
|
+
function getSessionSpawnCount(session_id) {
|
|
903
|
+
const db = getDb();
|
|
904
|
+
const row = db.query("SELECT agent_spawns FROM sessions WHERE session_id = ?").get(session_id);
|
|
905
|
+
return row?.agent_spawns || 0;
|
|
906
|
+
}
|
|
907
|
+
function periodKey(type) {
|
|
908
|
+
const now = new Date;
|
|
909
|
+
if (type === "daily")
|
|
910
|
+
return `daily:${now.toISOString().slice(0, 10)}`;
|
|
911
|
+
if (type === "weekly") {
|
|
912
|
+
const d = new Date(now);
|
|
913
|
+
d.setDate(d.getDate() - d.getDay());
|
|
914
|
+
return `weekly:${d.toISOString().slice(0, 10)}`;
|
|
915
|
+
}
|
|
916
|
+
return `monthly:${now.toISOString().slice(0, 7)}`;
|
|
917
|
+
}
|
|
918
|
+
function addToBudget(cost, limits) {
|
|
919
|
+
const db = getDb();
|
|
920
|
+
const now = new Date().toISOString();
|
|
921
|
+
for (const [type, limit] of [["daily", limits.daily], ["weekly", limits.weekly], ["monthly", limits.monthly]]) {
|
|
922
|
+
const key = periodKey(type);
|
|
923
|
+
const existing = db.query("SELECT * FROM budget_state WHERE period = ?").get(key);
|
|
924
|
+
if (existing) {
|
|
925
|
+
db.run("UPDATE budget_state SET spent_usd = spent_usd + ?, last_updated = ? WHERE period = ?", [cost, now, key]);
|
|
926
|
+
} else {
|
|
927
|
+
db.run("INSERT INTO budget_state (period, spent_usd, limit_usd, last_updated) VALUES (?, ?, ?, ?)", [key, cost, limit, now]);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function getBudgetState(type, limit) {
|
|
932
|
+
const db = getDb();
|
|
933
|
+
const key = periodKey(type);
|
|
934
|
+
const row = db.query("SELECT * FROM budget_state WHERE period = ?").get(key);
|
|
935
|
+
const spent = row?.spent_usd || 0;
|
|
936
|
+
return {
|
|
937
|
+
period: key,
|
|
938
|
+
spent_usd: spent,
|
|
939
|
+
limit_usd: limit,
|
|
940
|
+
remaining_usd: Math.max(0, limit - spent),
|
|
941
|
+
percent: limit > 0 ? Math.min(100, spent / limit * 100) : 0
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function getDailySpend() {
|
|
945
|
+
const db = getDb();
|
|
946
|
+
const key = periodKey("daily");
|
|
947
|
+
const row = db.query("SELECT spent_usd FROM budget_state WHERE period = ?").get(key);
|
|
948
|
+
return row?.spent_usd || 0;
|
|
949
|
+
}
|
|
950
|
+
function getWindowBudget(window_type, messages_limit, tokens_limit) {
|
|
951
|
+
const db = getDb();
|
|
952
|
+
const now = Date.now();
|
|
953
|
+
const windowMs = window_type === "5h" ? 5 * 60 * 60 * 1000 : 7 * 24 * 60 * 60 * 1000;
|
|
954
|
+
const windowStart = new Date(now - windowMs).toISOString();
|
|
955
|
+
const windowEnd = new Date(now + windowMs).toISOString();
|
|
956
|
+
const row = db.query(`
|
|
957
|
+
SELECT
|
|
958
|
+
COUNT(*) as message_count,
|
|
959
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens), 0) as total_tokens
|
|
960
|
+
FROM usage_log
|
|
961
|
+
WHERE timestamp >= ?
|
|
962
|
+
`).get(windowStart);
|
|
963
|
+
const messages_used = row?.message_count || 0;
|
|
964
|
+
const tokens_used = row?.total_tokens || 0;
|
|
965
|
+
const msgPct = messages_limit > 0 ? messages_used / messages_limit * 100 : 0;
|
|
966
|
+
const tokPct = tokens_limit > 0 ? tokens_used / tokens_limit * 100 : 0;
|
|
967
|
+
const percent = Math.min(100, Math.max(msgPct, tokPct));
|
|
968
|
+
const oldestInWindow = db.query(`
|
|
969
|
+
SELECT MIN(timestamp) as oldest FROM usage_log WHERE timestamp >= ?
|
|
970
|
+
`).get(windowStart);
|
|
971
|
+
let time_to_reset_s = Math.floor(windowMs / 1000);
|
|
972
|
+
if (oldestInWindow?.oldest) {
|
|
973
|
+
const oldestMs = new Date(oldestInWindow.oldest).getTime();
|
|
974
|
+
time_to_reset_s = Math.max(0, Math.floor((oldestMs + windowMs - now) / 1000));
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
window_type,
|
|
978
|
+
window_start: windowStart,
|
|
979
|
+
window_end: windowEnd,
|
|
980
|
+
messages_used,
|
|
981
|
+
messages_limit,
|
|
982
|
+
tokens_used,
|
|
983
|
+
tokens_limit,
|
|
984
|
+
percent,
|
|
985
|
+
time_to_reset_s
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function addAlert(alert) {
|
|
989
|
+
const db = getDb();
|
|
990
|
+
db.run("INSERT INTO alerts (type, severity, message, session_id, timestamp, acknowledged) VALUES (?, ?, ?, ?, ?, 0)", [alert.type, alert.severity, alert.message, alert.session_id || null, alert.timestamp]);
|
|
991
|
+
}
|
|
992
|
+
function getRecentAlerts(limit = 50) {
|
|
993
|
+
const db = getDb();
|
|
994
|
+
return db.query("SELECT * FROM alerts ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
995
|
+
}
|
|
996
|
+
function createKavachApproval(approval) {
|
|
997
|
+
const db = getDb();
|
|
998
|
+
const record = {
|
|
999
|
+
...approval,
|
|
1000
|
+
status: "pending",
|
|
1001
|
+
first_approver: null,
|
|
1002
|
+
decided_at: null,
|
|
1003
|
+
decided_by: null,
|
|
1004
|
+
notified: false
|
|
1005
|
+
};
|
|
1006
|
+
db.run(`INSERT INTO kavach_approvals (id, created_at, command, tool_name, level, consequence, session_id, status, first_approver, decided_at, decided_by, notified, timeout_ms)
|
|
1007
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, NULL, 0, ?)`, [record.id, record.created_at, record.command, record.tool_name, record.level, record.consequence, record.session_id, record.timeout_ms]);
|
|
1008
|
+
return record;
|
|
1009
|
+
}
|
|
1010
|
+
function getKavachApproval(id) {
|
|
1011
|
+
const db = getDb();
|
|
1012
|
+
return db.query("SELECT * FROM kavach_approvals WHERE id = ?").get(id);
|
|
1013
|
+
}
|
|
1014
|
+
function getPendingApprovals() {
|
|
1015
|
+
const db = getDb();
|
|
1016
|
+
return db.query("SELECT * FROM kavach_approvals WHERE status IN ('pending', 'pending_second') ORDER BY created_at DESC").all();
|
|
1017
|
+
}
|
|
1018
|
+
function decideKavachApproval(id, decision, decidedBy, opts = { dual_control: false, require_different_approvers: false }) {
|
|
1019
|
+
const db = getDb();
|
|
1020
|
+
const now = new Date().toISOString();
|
|
1021
|
+
const approval = getKavachApproval(id);
|
|
1022
|
+
if (!approval)
|
|
1023
|
+
return false;
|
|
1024
|
+
if (decision === "STOP" || decision === "TIMEOUT") {
|
|
1025
|
+
const status = decision === "STOP" ? "stopped" : "timed_out";
|
|
1026
|
+
const result = db.run("UPDATE kavach_approvals SET status = ?, decided_at = ?, decided_by = ? WHERE id = ? AND status IN ('pending', 'pending_second')", [status, now, decidedBy, id]);
|
|
1027
|
+
return (result.changes ?? 0) > 0;
|
|
1028
|
+
}
|
|
1029
|
+
if (decision === "EXPLAIN") {
|
|
1030
|
+
const result = db.run("UPDATE kavach_approvals SET status = 'explained', decided_at = ?, decided_by = ? WHERE id = ? AND status = 'pending'", [now, decidedBy, id]);
|
|
1031
|
+
return (result.changes ?? 0) > 0;
|
|
1032
|
+
}
|
|
1033
|
+
if (approval.status === "pending") {
|
|
1034
|
+
const isDualControl = opts.dual_control && approval.level === 4;
|
|
1035
|
+
if (isDualControl) {
|
|
1036
|
+
const result2 = db.run("UPDATE kavach_approvals SET status = 'pending_second', first_approver = ? WHERE id = ? AND status = 'pending'", [decidedBy, id]);
|
|
1037
|
+
return (result2.changes ?? 0) > 0;
|
|
1038
|
+
}
|
|
1039
|
+
const result = db.run("UPDATE kavach_approvals SET status = 'allowed', decided_at = ?, decided_by = ? WHERE id = ? AND status = 'pending'", [now, decidedBy, id]);
|
|
1040
|
+
return (result.changes ?? 0) > 0;
|
|
1041
|
+
}
|
|
1042
|
+
if (approval.status === "pending_second" && decision === "ALLOW") {
|
|
1043
|
+
if (opts.require_different_approvers && approval.first_approver === decidedBy) {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
const result = db.run("UPDATE kavach_approvals SET status = 'allowed', decided_at = ?, decided_by = ? WHERE id = ? AND status = 'pending_second'", [now, decidedBy, id]);
|
|
1047
|
+
return (result.changes ?? 0) > 0;
|
|
1048
|
+
}
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
function markKavachNotified(id) {
|
|
1052
|
+
const db = getDb();
|
|
1053
|
+
db.run("UPDATE kavach_approvals SET notified = 1 WHERE id = ?", [id]);
|
|
1054
|
+
}
|
|
1055
|
+
function getRecentApprovals(limit = 20) {
|
|
1056
|
+
const db = getDb();
|
|
1057
|
+
return db.query("SELECT * FROM kavach_approvals ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1058
|
+
}
|
|
1059
|
+
function upsertAgent(agent) {
|
|
1060
|
+
const db = getDb();
|
|
1061
|
+
db.run(`INSERT INTO agents (
|
|
1062
|
+
agent_id, state, identity_confidence, parent_id, session_id, depth,
|
|
1063
|
+
budget_cap_usd, budget_used_usd, budget_pool_reserved,
|
|
1064
|
+
tool_calls, loop_count, tools_declared, violation_count,
|
|
1065
|
+
spawn_timestamp, last_seen, policy_path, stop_requested,
|
|
1066
|
+
quarantine_reason, quarantine_rule, release_reason, released_by, resume_manifest_path
|
|
1067
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1068
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
1069
|
+
state = excluded.state,
|
|
1070
|
+
identity_confidence = excluded.identity_confidence,
|
|
1071
|
+
parent_id = excluded.parent_id,
|
|
1072
|
+
budget_used_usd = excluded.budget_used_usd,
|
|
1073
|
+
budget_pool_reserved = excluded.budget_pool_reserved,
|
|
1074
|
+
tool_calls = excluded.tool_calls,
|
|
1075
|
+
loop_count = excluded.loop_count,
|
|
1076
|
+
tools_declared = excluded.tools_declared,
|
|
1077
|
+
violation_count = excluded.violation_count,
|
|
1078
|
+
last_seen = excluded.last_seen,
|
|
1079
|
+
stop_requested = excluded.stop_requested,
|
|
1080
|
+
quarantine_reason = excluded.quarantine_reason,
|
|
1081
|
+
quarantine_rule = excluded.quarantine_rule,
|
|
1082
|
+
release_reason = excluded.release_reason,
|
|
1083
|
+
released_by = excluded.released_by,
|
|
1084
|
+
resume_manifest_path = excluded.resume_manifest_path`, [
|
|
1085
|
+
agent.agent_id,
|
|
1086
|
+
agent.state,
|
|
1087
|
+
agent.identity_confidence,
|
|
1088
|
+
agent.parent_id,
|
|
1089
|
+
agent.session_id,
|
|
1090
|
+
agent.depth,
|
|
1091
|
+
agent.budget_cap_usd,
|
|
1092
|
+
agent.budget_used_usd,
|
|
1093
|
+
agent.budget_pool_reserved,
|
|
1094
|
+
agent.tool_calls,
|
|
1095
|
+
agent.loop_count,
|
|
1096
|
+
agent.tools_declared,
|
|
1097
|
+
agent.violation_count,
|
|
1098
|
+
agent.spawn_timestamp,
|
|
1099
|
+
agent.last_seen,
|
|
1100
|
+
agent.policy_path,
|
|
1101
|
+
agent.stop_requested ? 1 : 0,
|
|
1102
|
+
agent.quarantine_reason,
|
|
1103
|
+
agent.quarantine_rule,
|
|
1104
|
+
agent.release_reason,
|
|
1105
|
+
agent.released_by,
|
|
1106
|
+
agent.resume_manifest_path
|
|
1107
|
+
]);
|
|
1108
|
+
}
|
|
1109
|
+
function touchAgent(agentId) {
|
|
1110
|
+
const db = getDb();
|
|
1111
|
+
const now = new Date().toISOString();
|
|
1112
|
+
db.run("UPDATE agents SET last_seen = ?, tool_calls = tool_calls + 1, loop_count = loop_count + 1 WHERE agent_id = ? AND state = 'RUNNING'", [now, agentId]);
|
|
1113
|
+
}
|
|
1114
|
+
function setAgentState(agentId, state, meta = {}) {
|
|
1115
|
+
const db = getDb();
|
|
1116
|
+
const now = new Date().toISOString();
|
|
1117
|
+
db.run(`UPDATE agents SET state = ?, last_seen = ?,
|
|
1118
|
+
quarantine_reason = CASE WHEN ? = 'QUARANTINED' THEN ? ELSE quarantine_reason END,
|
|
1119
|
+
quarantine_rule = CASE WHEN ? = 'QUARANTINED' THEN ? ELSE quarantine_rule END,
|
|
1120
|
+
release_reason = CASE WHEN ? = 'RUNNING' THEN ? ELSE release_reason END,
|
|
1121
|
+
released_by = CASE WHEN ? = 'RUNNING' THEN ? ELSE released_by END,
|
|
1122
|
+
resume_manifest_path = COALESCE(?, resume_manifest_path)
|
|
1123
|
+
WHERE agent_id = ?`, [
|
|
1124
|
+
state,
|
|
1125
|
+
now,
|
|
1126
|
+
state,
|
|
1127
|
+
meta.reason ?? null,
|
|
1128
|
+
state,
|
|
1129
|
+
meta.rule ?? null,
|
|
1130
|
+
state,
|
|
1131
|
+
meta.reason ?? null,
|
|
1132
|
+
state,
|
|
1133
|
+
meta.released_by ?? null,
|
|
1134
|
+
meta.resume_manifest_path ?? null,
|
|
1135
|
+
agentId
|
|
1136
|
+
]);
|
|
1137
|
+
}
|
|
1138
|
+
function requestStop(agentId) {
|
|
1139
|
+
const db = getDb();
|
|
1140
|
+
db.run("UPDATE agents SET stop_requested = 1 WHERE agent_id = ?", [agentId]);
|
|
1141
|
+
}
|
|
1142
|
+
function isStopRequested(agentId) {
|
|
1143
|
+
const db = getDb();
|
|
1144
|
+
const row = db.query("SELECT stop_requested FROM agents WHERE agent_id = ?").get(agentId);
|
|
1145
|
+
return (row?.stop_requested ?? 0) === 1;
|
|
1146
|
+
}
|
|
1147
|
+
function getAgentRow(agentId) {
|
|
1148
|
+
const db = getDb();
|
|
1149
|
+
return db.query("SELECT * FROM agents WHERE agent_id = ?").get(agentId);
|
|
1150
|
+
}
|
|
1151
|
+
function listAgentRows(states) {
|
|
1152
|
+
const db = getDb();
|
|
1153
|
+
if (!states || states.length === 0) {
|
|
1154
|
+
return db.query("SELECT * FROM agents ORDER BY spawn_timestamp DESC").all();
|
|
1155
|
+
}
|
|
1156
|
+
const placeholders = states.map(() => "?").join(",");
|
|
1157
|
+
return db.query(`SELECT * FROM agents WHERE state IN (${placeholders}) ORDER BY spawn_timestamp DESC`).all(...states);
|
|
1158
|
+
}
|
|
1159
|
+
function incrementViolationCount(agentId) {
|
|
1160
|
+
const db = getDb();
|
|
1161
|
+
db.run("UPDATE agents SET violation_count = violation_count + 1 WHERE agent_id = ?", [agentId]);
|
|
1162
|
+
const row = db.query("SELECT violation_count FROM agents WHERE agent_id = ?").get(agentId);
|
|
1163
|
+
return row?.violation_count ?? 0;
|
|
1164
|
+
}
|
|
1165
|
+
function recordAgentUsage(opts) {
|
|
1166
|
+
const db = getDb();
|
|
1167
|
+
const now = new Date().toISOString();
|
|
1168
|
+
db.run("UPDATE agents SET budget_used_usd = budget_used_usd + ? WHERE agent_id = ?", [opts.cost_usd, opts.agent_id]);
|
|
1169
|
+
db.run(`INSERT INTO usage_log (session_id, timestamp, model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, estimated_cost_usd, is_agent_spawn, raw_json)
|
|
1170
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`, [
|
|
1171
|
+
opts.agent_id,
|
|
1172
|
+
now,
|
|
1173
|
+
opts.model,
|
|
1174
|
+
opts.input_tokens,
|
|
1175
|
+
opts.output_tokens,
|
|
1176
|
+
opts.cache_read_tokens ?? 0,
|
|
1177
|
+
opts.cache_creation_tokens ?? 0,
|
|
1178
|
+
opts.cost_usd,
|
|
1179
|
+
null
|
|
1180
|
+
]);
|
|
1181
|
+
}
|
|
1182
|
+
function checkBudgetInheritance(opts) {
|
|
1183
|
+
if (opts.child_cap_usd <= 0)
|
|
1184
|
+
return { allowed: true };
|
|
1185
|
+
const db = getDb();
|
|
1186
|
+
const parent = db.query("SELECT budget_cap_usd, budget_used_usd, budget_pool_reserved FROM agents WHERE agent_id = ?").get(opts.parent_id);
|
|
1187
|
+
if (!parent)
|
|
1188
|
+
return { allowed: true };
|
|
1189
|
+
const parentRemaining = parent.budget_cap_usd - parent.budget_used_usd - parent.budget_pool_reserved;
|
|
1190
|
+
if (parentRemaining < opts.child_cap_usd) {
|
|
1191
|
+
return {
|
|
1192
|
+
allowed: false,
|
|
1193
|
+
error: `Budget inheritance rejected: parent ${opts.parent_id} remaining $${parentRemaining.toFixed(4)} < child cap $${opts.child_cap_usd.toFixed(4)} (KAV-018)`
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
db.run("UPDATE agents SET budget_pool_reserved = budget_pool_reserved + ? WHERE agent_id = ?", [opts.child_cap_usd, opts.parent_id]);
|
|
1197
|
+
return { allowed: true };
|
|
1198
|
+
}
|
|
1199
|
+
function rebalanceBudget(agentId) {
|
|
1200
|
+
const db = getDb();
|
|
1201
|
+
const agent = db.query("SELECT parent_id, budget_cap_usd, budget_used_usd FROM agents WHERE agent_id = ?").get(agentId);
|
|
1202
|
+
if (!agent || !agent.parent_id || agent.budget_cap_usd <= 0)
|
|
1203
|
+
return;
|
|
1204
|
+
const unused = Math.max(0, agent.budget_cap_usd - agent.budget_used_usd);
|
|
1205
|
+
if (unused <= 0)
|
|
1206
|
+
return;
|
|
1207
|
+
db.run(`UPDATE agents SET
|
|
1208
|
+
budget_pool_reserved = MAX(0, budget_pool_reserved - ?),
|
|
1209
|
+
budget_used_usd = budget_used_usd + ?
|
|
1210
|
+
WHERE agent_id = ?`, [agent.budget_cap_usd, -unused, agent.parent_id]);
|
|
1211
|
+
}
|
|
1212
|
+
function getAgentCostProjection(agentId) {
|
|
1213
|
+
const db = getDb();
|
|
1214
|
+
const agent = db.query("SELECT budget_cap_usd, budget_used_usd, tool_calls FROM agents WHERE agent_id = ?").get(agentId);
|
|
1215
|
+
if (!agent || agent.budget_cap_usd <= 0)
|
|
1216
|
+
return null;
|
|
1217
|
+
const avgCostPerCall = agent.tool_calls > 0 ? agent.budget_used_usd / agent.tool_calls : 0;
|
|
1218
|
+
const projected = agent.budget_used_usd + avgCostPerCall * agent.tool_calls * 0.3;
|
|
1219
|
+
const pct = projected / agent.budget_cap_usd * 100;
|
|
1220
|
+
return {
|
|
1221
|
+
budget_used_usd: agent.budget_used_usd,
|
|
1222
|
+
budget_cap_usd: agent.budget_cap_usd,
|
|
1223
|
+
projected_total_usd: projected,
|
|
1224
|
+
pct_of_cap: pct,
|
|
1225
|
+
alert_level: pct >= 95 ? "soft_stop" : pct >= 80 ? "warn" : "ok"
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
function getCostTree(sessionId) {
|
|
1229
|
+
const db = getDb();
|
|
1230
|
+
const rows = sessionId ? db.query("SELECT * FROM agents WHERE session_id = ? ORDER BY depth, spawn_timestamp").all(sessionId) : db.query("SELECT * FROM agents ORDER BY depth, spawn_timestamp DESC LIMIT 200").all();
|
|
1231
|
+
const nodeMap = new Map;
|
|
1232
|
+
const roots = [];
|
|
1233
|
+
for (const row of rows) {
|
|
1234
|
+
const node = {
|
|
1235
|
+
agent_id: row.agent_id,
|
|
1236
|
+
state: row.state,
|
|
1237
|
+
depth: row.depth,
|
|
1238
|
+
budget_cap_usd: row.budget_cap_usd,
|
|
1239
|
+
budget_used_usd: row.budget_used_usd,
|
|
1240
|
+
budget_pool_reserved: row.budget_pool_reserved,
|
|
1241
|
+
tool_calls: row.tool_calls,
|
|
1242
|
+
violation_count: row.violation_count,
|
|
1243
|
+
children: []
|
|
1244
|
+
};
|
|
1245
|
+
nodeMap.set(row.agent_id, node);
|
|
1246
|
+
}
|
|
1247
|
+
for (const row of rows) {
|
|
1248
|
+
const node = nodeMap.get(row.agent_id);
|
|
1249
|
+
if (row.parent_id && nodeMap.has(row.parent_id)) {
|
|
1250
|
+
nodeMap.get(row.parent_id).children.push(node);
|
|
1251
|
+
} else {
|
|
1252
|
+
roots.push(node);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return roots;
|
|
1256
|
+
}
|
|
1257
|
+
var _db = null;
|
|
1258
|
+
var init_db = __esm(() => {
|
|
1259
|
+
init_config();
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
// ../../src/kernel/profile-store.ts
|
|
1263
|
+
var exports_profile_store = {};
|
|
1264
|
+
__export(exports_profile_store, {
|
|
1265
|
+
verifyReceiptChain: () => verifyReceiptChain,
|
|
1266
|
+
storeProfile: () => storeProfile,
|
|
1267
|
+
recordKernelReceipt: () => recordKernelReceipt,
|
|
1268
|
+
getReceiptChain: () => getReceiptChain,
|
|
1269
|
+
getProfileForAgent: () => getProfileForAgent,
|
|
1270
|
+
getProfile: () => getProfile,
|
|
1271
|
+
ensureKernelSchema: () => ensureKernelSchema,
|
|
1272
|
+
checkProfileDrift: () => checkProfileDrift
|
|
1273
|
+
});
|
|
1274
|
+
import { createHash as createHash2 } from "crypto";
|
|
1275
|
+
function ensureKernelSchema() {
|
|
1276
|
+
const db = getDb();
|
|
1277
|
+
db.exec(`
|
|
1278
|
+
CREATE TABLE IF NOT EXISTS kernel_profiles (
|
|
1279
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1280
|
+
session_id TEXT NOT NULL,
|
|
1281
|
+
agent_id TEXT,
|
|
1282
|
+
profile_hash TEXT NOT NULL,
|
|
1283
|
+
trust_mask INTEGER NOT NULL,
|
|
1284
|
+
domain TEXT NOT NULL,
|
|
1285
|
+
agent_type TEXT NOT NULL DEFAULT 'claude-code',
|
|
1286
|
+
syscall_count INTEGER NOT NULL,
|
|
1287
|
+
profile_json TEXT NOT NULL,
|
|
1288
|
+
stored_at TEXT NOT NULL,
|
|
1289
|
+
profile_path TEXT,
|
|
1290
|
+
UNIQUE(session_id)
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
CREATE TABLE IF NOT EXISTS kernel_receipts (
|
|
1294
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1295
|
+
receipt_id TEXT NOT NULL UNIQUE,
|
|
1296
|
+
session_id TEXT NOT NULL,
|
|
1297
|
+
agent_id TEXT,
|
|
1298
|
+
event_type TEXT NOT NULL,
|
|
1299
|
+
syscall TEXT,
|
|
1300
|
+
falco_rule TEXT,
|
|
1301
|
+
severity TEXT NOT NULL DEFAULT 'WARN',
|
|
1302
|
+
violation_details TEXT,
|
|
1303
|
+
profile_hash TEXT,
|
|
1304
|
+
receipt_hash TEXT NOT NULL,
|
|
1305
|
+
prev_receipt_hash TEXT,
|
|
1306
|
+
sealed_at TEXT NOT NULL
|
|
1307
|
+
);
|
|
1308
|
+
|
|
1309
|
+
CREATE TABLE IF NOT EXISTS kernel_drift_events (
|
|
1310
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1311
|
+
session_id TEXT NOT NULL,
|
|
1312
|
+
stored_hash TEXT NOT NULL,
|
|
1313
|
+
actual_hash TEXT NOT NULL,
|
|
1314
|
+
detected_at TEXT NOT NULL
|
|
1315
|
+
);
|
|
1316
|
+
`);
|
|
1317
|
+
}
|
|
1318
|
+
function storeProfile(sessionId, agentId, profile, profilePath = null) {
|
|
1319
|
+
ensureKernelSchema();
|
|
1320
|
+
const db = getDb();
|
|
1321
|
+
const profileJson = JSON.stringify(profile);
|
|
1322
|
+
const hash = createHash2("sha256").update(canonicalJson(profile)).digest("hex");
|
|
1323
|
+
const syscallCount = profile.syscalls[0]?.names.length ?? 0;
|
|
1324
|
+
db.run(`INSERT OR REPLACE INTO kernel_profiles
|
|
1325
|
+
(session_id, agent_id, profile_hash, trust_mask, domain, agent_type, syscall_count, profile_json, stored_at, profile_path)
|
|
1326
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1327
|
+
sessionId,
|
|
1328
|
+
agentId,
|
|
1329
|
+
hash,
|
|
1330
|
+
profile._kavachos.trust_mask,
|
|
1331
|
+
profile._kavachos.domain,
|
|
1332
|
+
profile._kavachos.agent_type,
|
|
1333
|
+
syscallCount,
|
|
1334
|
+
profileJson,
|
|
1335
|
+
new Date().toISOString(),
|
|
1336
|
+
profilePath
|
|
1337
|
+
]);
|
|
1338
|
+
return hash;
|
|
1339
|
+
}
|
|
1340
|
+
function getProfile(sessionId) {
|
|
1341
|
+
ensureKernelSchema();
|
|
1342
|
+
const db = getDb();
|
|
1343
|
+
return db.query("SELECT * FROM kernel_profiles WHERE session_id = ?").get(sessionId) ?? null;
|
|
1344
|
+
}
|
|
1345
|
+
function getProfileForAgent(agentId, sessionId) {
|
|
1346
|
+
ensureKernelSchema();
|
|
1347
|
+
const db = getDb();
|
|
1348
|
+
if (sessionId) {
|
|
1349
|
+
return db.query("SELECT * FROM kernel_profiles WHERE agent_id = ? AND session_id = ? ORDER BY stored_at DESC LIMIT 1").get(agentId, sessionId) ?? null;
|
|
1350
|
+
}
|
|
1351
|
+
return db.query("SELECT * FROM kernel_profiles WHERE agent_id = ? ORDER BY stored_at DESC LIMIT 1").get(agentId) ?? null;
|
|
1352
|
+
}
|
|
1353
|
+
function checkProfileDrift(sessionId) {
|
|
1354
|
+
const stored = getProfile(sessionId);
|
|
1355
|
+
if (!stored)
|
|
1356
|
+
return null;
|
|
1357
|
+
const profile = JSON.parse(stored.profile_json);
|
|
1358
|
+
const actualHash = createHash2("sha256").update(canonicalJson(profile)).digest("hex");
|
|
1359
|
+
if (actualHash !== stored.profile_hash) {
|
|
1360
|
+
const event = {
|
|
1361
|
+
session_id: sessionId,
|
|
1362
|
+
stored_hash: stored.profile_hash,
|
|
1363
|
+
actual_hash: actualHash,
|
|
1364
|
+
detected_at: new Date().toISOString()
|
|
1365
|
+
};
|
|
1366
|
+
const db = getDb();
|
|
1367
|
+
db.run("INSERT INTO kernel_drift_events (session_id, stored_hash, actual_hash, detected_at) VALUES (?, ?, ?, ?)", [event.session_id, event.stored_hash, event.actual_hash, event.detected_at]);
|
|
1368
|
+
return event;
|
|
1369
|
+
}
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
function recordKernelReceipt(receiptId, sessionId, agentId, eventType, details) {
|
|
1373
|
+
ensureKernelSchema();
|
|
1374
|
+
const db = getDb();
|
|
1375
|
+
db.run(`INSERT OR IGNORE INTO kernel_receipts
|
|
1376
|
+
(receipt_id, session_id, agent_id, event_type, syscall, falco_rule, severity,
|
|
1377
|
+
violation_details, profile_hash, receipt_hash, prev_receipt_hash, sealed_at)
|
|
1378
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1379
|
+
receiptId,
|
|
1380
|
+
sessionId,
|
|
1381
|
+
agentId,
|
|
1382
|
+
eventType,
|
|
1383
|
+
details.syscall ?? null,
|
|
1384
|
+
details.falco_rule ?? null,
|
|
1385
|
+
details.severity ?? "WARN",
|
|
1386
|
+
details.violation_details ?? null,
|
|
1387
|
+
details.profile_hash ?? null,
|
|
1388
|
+
details.receipt_hash,
|
|
1389
|
+
details.prev_receipt_hash ?? null,
|
|
1390
|
+
new Date().toISOString()
|
|
1391
|
+
]);
|
|
1392
|
+
}
|
|
1393
|
+
function getReceiptChain(sessionId) {
|
|
1394
|
+
ensureKernelSchema();
|
|
1395
|
+
const db = getDb();
|
|
1396
|
+
return db.query("SELECT receipt_id, receipt_hash, prev_receipt_hash, sealed_at FROM kernel_receipts WHERE session_id = ? ORDER BY id ASC").all(sessionId);
|
|
1397
|
+
}
|
|
1398
|
+
function verifyReceiptChain(sessionId) {
|
|
1399
|
+
const chain = getReceiptChain(sessionId);
|
|
1400
|
+
if (chain.length === 0)
|
|
1401
|
+
return { valid: true, receipt_count: 0 };
|
|
1402
|
+
let prevHash = null;
|
|
1403
|
+
for (const receipt of chain) {
|
|
1404
|
+
if (receipt.prev_receipt_hash !== prevHash) {
|
|
1405
|
+
return { valid: false, gap_at: receipt.receipt_id, receipt_count: chain.length };
|
|
1406
|
+
}
|
|
1407
|
+
prevHash = receipt.receipt_hash;
|
|
1408
|
+
}
|
|
1409
|
+
return { valid: true, receipt_count: chain.length };
|
|
1410
|
+
}
|
|
1411
|
+
var init_profile_store = __esm(() => {
|
|
1412
|
+
init_db();
|
|
1413
|
+
init_seccomp_profile_generator();
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
// ../../src/kernel/kernel-receipt.ts
|
|
1417
|
+
import { createHash as createHash3, randomBytes } from "crypto";
|
|
1418
|
+
function sealKernelViolation(event) {
|
|
1419
|
+
const receiptId = `KOS-PRAMANA-${randomBytes(8).toString("hex").toUpperCase()}`;
|
|
1420
|
+
const sealedAt = new Date().toISOString();
|
|
1421
|
+
const chain = getReceiptChain(event.session_id);
|
|
1422
|
+
const prevHash = chain.length > 0 ? chain[chain.length - 1].receipt_hash : null;
|
|
1423
|
+
const receiptHash = createHash3("sha256").update(`${receiptId}|${event.session_id}|${event.event_type}|${sealedAt}|${prevHash ?? "GENESIS"}`).digest("hex");
|
|
1424
|
+
recordKernelReceipt(receiptId, event.session_id, event.agent_id ?? null, event.event_type, {
|
|
1425
|
+
syscall: event.syscall,
|
|
1426
|
+
falco_rule: event.falco_rule,
|
|
1427
|
+
severity: event.severity ?? "WARN",
|
|
1428
|
+
violation_details: event.violation_details,
|
|
1429
|
+
profile_hash: event.profile_hash,
|
|
1430
|
+
prev_receipt_hash: prevHash ?? undefined,
|
|
1431
|
+
receipt_hash: receiptHash
|
|
1432
|
+
});
|
|
1433
|
+
return {
|
|
1434
|
+
receipt_id: receiptId,
|
|
1435
|
+
session_id: event.session_id,
|
|
1436
|
+
event_type: event.event_type,
|
|
1437
|
+
receipt_hash: receiptHash,
|
|
1438
|
+
prev_receipt_hash: prevHash,
|
|
1439
|
+
sealed_at: sealedAt,
|
|
1440
|
+
pramana_version: "1.1",
|
|
1441
|
+
rule_ref: "KOS-005"
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
function parseFalcoEvent(line) {
|
|
1445
|
+
try {
|
|
1446
|
+
const parsed = JSON.parse(line);
|
|
1447
|
+
const priority = (parsed.priority ?? "WARNING").toUpperCase();
|
|
1448
|
+
const severity = priority === "CRITICAL" || priority === "ERROR" ? "CRITICAL" : priority === "WARNING" ? "WARN" : "WARN";
|
|
1449
|
+
return {
|
|
1450
|
+
session_id: parsed.output_fields?.proc_env?.KAVACHOS_SESSION_ID ?? "unknown",
|
|
1451
|
+
agent_id: parsed.output_fields?.proc_env?.KAVACHOS_AGENT_ID ?? null,
|
|
1452
|
+
event_type: "FALCO_ALERT",
|
|
1453
|
+
falco_rule: parsed.rule,
|
|
1454
|
+
violation_details: parsed.output,
|
|
1455
|
+
severity
|
|
1456
|
+
};
|
|
1457
|
+
} catch {
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
function checkViolationRate(events, windowMs = 60000) {
|
|
1462
|
+
const cutoff = Date.now() - windowMs;
|
|
1463
|
+
const recent = events.filter((e) => new Date(e.sealed_at).getTime() > cutoff);
|
|
1464
|
+
return recent.length > 5;
|
|
1465
|
+
}
|
|
1466
|
+
var init_kernel_receipt = __esm(() => {
|
|
1467
|
+
init_profile_store();
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// ../../src/kernel/egress-policy.ts
|
|
1471
|
+
function buildEgressPolicy(trustMask, domain) {
|
|
1472
|
+
const allow = [...BASE_ALLOW];
|
|
1473
|
+
const extras = DOMAIN_EXTRA[domain] ?? DOMAIN_EXTRA.general;
|
|
1474
|
+
allow.push(...extras);
|
|
1475
|
+
for (let bit = 0;bit < 32; bit++) {
|
|
1476
|
+
if (trustMask & 1 << bit) {
|
|
1477
|
+
const extra = TRUST_MASK_EGRESS[bit] ?? [];
|
|
1478
|
+
allow.push(...extra);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (!(trustMask & 1 << 6)) {
|
|
1482
|
+
allow.push({ host: "127.0.0.1", port: 0, note: "loopback" });
|
|
1483
|
+
allow.push({ host: "::1", port: 0, note: "loopback IPv6" });
|
|
1484
|
+
}
|
|
1485
|
+
return { domain, trust_mask: trustMask, allow };
|
|
1486
|
+
}
|
|
1487
|
+
function serialiseEgressPolicy(policy) {
|
|
1488
|
+
return JSON.stringify(policy, null, 2);
|
|
1489
|
+
}
|
|
1490
|
+
var BASE_ALLOW, DOMAIN_EXTRA, TRUST_MASK_EGRESS;
|
|
1491
|
+
var init_egress_policy = __esm(() => {
|
|
1492
|
+
BASE_ALLOW = [
|
|
1493
|
+
{ host: "api.anthropic.com", port: 443, note: "Anthropic API" },
|
|
1494
|
+
{ host: "api.openai.com", port: 443, note: "OpenAI API" },
|
|
1495
|
+
{ host: "generativelanguage.googleapis.com", port: 443, note: "Gemini API" },
|
|
1496
|
+
{ host: "api.groq.com", port: 443, note: "Groq API (free_first)" },
|
|
1497
|
+
{ host: "api-inference.huggingface.co", port: 443, note: "HF Inference (free_first)" }
|
|
1498
|
+
];
|
|
1499
|
+
DOMAIN_EXTRA = {
|
|
1500
|
+
general: [
|
|
1501
|
+
{ host: "github.com", port: 443, note: "GitHub API" },
|
|
1502
|
+
{ host: "raw.githubusercontent.com", port: 443, note: "GitHub raw" },
|
|
1503
|
+
{ host: "registry.npmjs.org", port: 443, note: "npm registry" }
|
|
1504
|
+
],
|
|
1505
|
+
maritime: [
|
|
1506
|
+
{ host: "api.aisstream.io", port: 443, note: "AIS stream" },
|
|
1507
|
+
{ host: "maddox.iho.int", port: 443, note: "IHO chart service" },
|
|
1508
|
+
{ host: "127.0.0.1", port: 0, note: "localhost (NMEA/Modbus)" }
|
|
1509
|
+
],
|
|
1510
|
+
logistics: [
|
|
1511
|
+
{ host: "api.searates.com", port: 443, note: "freight rates" },
|
|
1512
|
+
{ host: "api.bolero.net", port: 443, note: "eBL platform" }
|
|
1513
|
+
],
|
|
1514
|
+
ot: [
|
|
1515
|
+
{ host: "127.0.0.1", port: 0, note: "localhost (Modbus/NMEA/AIS)" }
|
|
1516
|
+
],
|
|
1517
|
+
finance: [
|
|
1518
|
+
{ host: "api.stripe.com", port: 443, note: "Stripe" },
|
|
1519
|
+
{ host: "sandbox.hsm.example", port: 443, note: "HSM API" }
|
|
1520
|
+
]
|
|
1521
|
+
};
|
|
1522
|
+
TRUST_MASK_EGRESS = {
|
|
1523
|
+
4: [{ host: "smtp.mailgun.org", port: 587, note: "Mailgun SMTP" }],
|
|
1524
|
+
6: [
|
|
1525
|
+
{ host: "localhost", port: 0, note: "localhost" },
|
|
1526
|
+
{ host: "127.0.0.1", port: 0, note: "localhost" }
|
|
1527
|
+
]
|
|
1528
|
+
};
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
// ../../src/kernel/runner.ts
|
|
1532
|
+
var exports_runner = {};
|
|
1533
|
+
__export(exports_runner, {
|
|
1534
|
+
runWithKernel: () => runWithKernel,
|
|
1535
|
+
generateOnly: () => generateOnly
|
|
1536
|
+
});
|
|
1537
|
+
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, unlinkSync } from "fs";
|
|
1538
|
+
import { join as join2, dirname } from "path";
|
|
1539
|
+
import { spawn } from "child_process";
|
|
1540
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1541
|
+
function ensureKavachosDir() {
|
|
1542
|
+
if (!existsSync2(KAVACHOS_DIR))
|
|
1543
|
+
mkdirSync2(KAVACHOS_DIR, { recursive: true });
|
|
1544
|
+
}
|
|
1545
|
+
async function runWithKernel(agentCommand, opts) {
|
|
1546
|
+
ensureKavachosDir();
|
|
1547
|
+
const sessionId = opts.sessionId ?? `KOS-${randomBytes2(6).toString("hex").toUpperCase()}`;
|
|
1548
|
+
const agentType = opts.agentType ?? "claude-code";
|
|
1549
|
+
const { profile, hash: profileHash, syscall_count } = generateSeccompProfile(opts.trustMask, opts.domain, agentType);
|
|
1550
|
+
if (opts.verbose) {
|
|
1551
|
+
console.error(profileSummary({ profile, hash: profileHash, syscall_count }));
|
|
1552
|
+
}
|
|
1553
|
+
const profilePath = join2(KAVACHOS_DIR, `${sessionId}.seccomp.json`);
|
|
1554
|
+
writeFileSync2(profilePath, JSON.stringify(profile, null, 2));
|
|
1555
|
+
storeProfile(sessionId, opts.agentId ?? null, profile, profilePath);
|
|
1556
|
+
let falcoRulesPath = null;
|
|
1557
|
+
if (opts.falcoEnabled) {
|
|
1558
|
+
const falcoRules = generateFalcoRules(opts.domain, opts.trustMask);
|
|
1559
|
+
falcoRulesPath = join2(KAVACHOS_DIR, `${sessionId}.falco.yaml`);
|
|
1560
|
+
writeFileSync2(falcoRulesPath, falcoRules.rules);
|
|
1561
|
+
if (opts.verbose) {
|
|
1562
|
+
console.error(`[kavachos] Falco rules written: ${falcoRulesPath} (${falcoRules.rule_count} rules)`);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
let egressPolicyPath = null;
|
|
1566
|
+
if (opts.egressEnabled !== false) {
|
|
1567
|
+
const egressPolicy = buildEgressPolicy(opts.trustMask, opts.domain);
|
|
1568
|
+
egressPolicyPath = join2(KAVACHOS_DIR, `${sessionId}.egress.json`);
|
|
1569
|
+
writeFileSync2(egressPolicyPath, serialiseEgressPolicy(egressPolicy));
|
|
1570
|
+
if (opts.verbose) {
|
|
1571
|
+
console.error(`[kavachos] Egress policy written: ${egressPolicyPath} (${egressPolicy.allow.length} hosts)`);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
if (opts.dryRun) {
|
|
1575
|
+
console.log(JSON.stringify({
|
|
1576
|
+
sessionId,
|
|
1577
|
+
profileHash,
|
|
1578
|
+
syscallCount: syscall_count,
|
|
1579
|
+
profilePath,
|
|
1580
|
+
falcoRulesPath,
|
|
1581
|
+
egressPolicyPath,
|
|
1582
|
+
dryRun: true
|
|
1583
|
+
}, null, 2));
|
|
1584
|
+
return { sessionId, profileHash, syscallCount: syscall_count, profilePath, falcoRulesPath, egressPolicyPath };
|
|
1585
|
+
}
|
|
1586
|
+
const env = {
|
|
1587
|
+
...process.env,
|
|
1588
|
+
KAVACHOS_SESSION_ID: sessionId,
|
|
1589
|
+
KAVACHOS_AGENT_ID: opts.agentId ?? sessionId,
|
|
1590
|
+
KAVACHOS_TRUST_MASK: opts.trustMask.toString(),
|
|
1591
|
+
KAVACHOS_DOMAIN: opts.domain
|
|
1592
|
+
};
|
|
1593
|
+
const launchArgs = [
|
|
1594
|
+
"python3",
|
|
1595
|
+
APPLY_SECCOMP_PY,
|
|
1596
|
+
profilePath,
|
|
1597
|
+
"--",
|
|
1598
|
+
...agentCommand
|
|
1599
|
+
];
|
|
1600
|
+
if (opts.verbose) {
|
|
1601
|
+
console.error(`[kavachos] Launching: ${launchArgs.join(" ")}`);
|
|
1602
|
+
}
|
|
1603
|
+
return new Promise((resolve, reject) => {
|
|
1604
|
+
const child = spawn(launchArgs[0], launchArgs.slice(1), {
|
|
1605
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
1606
|
+
env
|
|
1607
|
+
});
|
|
1608
|
+
const result = {
|
|
1609
|
+
sessionId,
|
|
1610
|
+
profileHash,
|
|
1611
|
+
syscallCount: syscall_count,
|
|
1612
|
+
profilePath,
|
|
1613
|
+
falcoRulesPath,
|
|
1614
|
+
egressPolicyPath,
|
|
1615
|
+
pid: child.pid
|
|
1616
|
+
};
|
|
1617
|
+
if (egressPolicyPath && child.pid) {
|
|
1618
|
+
const CGROUP_EGRESS_PY = join2(dirname(new URL(import.meta.url).pathname), "cgroup-egress.py");
|
|
1619
|
+
const egressSidecar = spawn("python3", [CGROUP_EGRESS_PY, sessionId, egressPolicyPath, String(child.pid)], {
|
|
1620
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
1621
|
+
});
|
|
1622
|
+
egressSidecar.stderr?.on("data", (d) => {
|
|
1623
|
+
const line = d.toString().trim();
|
|
1624
|
+
if (line)
|
|
1625
|
+
process.stderr.write(line + `
|
|
1626
|
+
`);
|
|
1627
|
+
});
|
|
1628
|
+
egressSidecar.on("error", (err) => {
|
|
1629
|
+
process.stderr.write(`[kavachos:egress] sidecar error: ${err.message}
|
|
1630
|
+
`);
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
const recentReceipts = [];
|
|
1634
|
+
child.stderr?.on("data", (data) => {
|
|
1635
|
+
const lines = data.toString().split(`
|
|
1636
|
+
`).filter(Boolean);
|
|
1637
|
+
for (const line of lines) {
|
|
1638
|
+
if (line.includes("[kavachos]")) {
|
|
1639
|
+
process.stderr.write(line + `
|
|
1640
|
+
`);
|
|
1641
|
+
}
|
|
1642
|
+
if (line.startsWith("{") && line.includes('"rule"')) {
|
|
1643
|
+
const event = parseFalcoEvent(line);
|
|
1644
|
+
if (event) {
|
|
1645
|
+
event.session_id = sessionId;
|
|
1646
|
+
const receipt = sealKernelViolation({ ...event, profile_hash: profileHash });
|
|
1647
|
+
recentReceipts.push(receipt);
|
|
1648
|
+
if (checkViolationRate(recentReceipts)) {
|
|
1649
|
+
process.stderr.write(`[kavachos] RATE_EXCEEDED: >5 violations/min for session ${sessionId}
|
|
1650
|
+
`);
|
|
1651
|
+
sealKernelViolation({
|
|
1652
|
+
session_id: sessionId,
|
|
1653
|
+
agent_id: opts.agentId ?? null,
|
|
1654
|
+
event_type: "RATE_EXCEEDED",
|
|
1655
|
+
violation_details: "Falco violation rate exceeded 5/min \u2014 potential low-and-slow exfil",
|
|
1656
|
+
profile_hash: profileHash,
|
|
1657
|
+
severity: "CRITICAL"
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
child.on("exit", (code) => {
|
|
1665
|
+
result.exitCode = code ?? 0;
|
|
1666
|
+
try {
|
|
1667
|
+
unlinkSync(profilePath);
|
|
1668
|
+
} catch {}
|
|
1669
|
+
if (egressPolicyPath) {
|
|
1670
|
+
try {
|
|
1671
|
+
unlinkSync(egressPolicyPath);
|
|
1672
|
+
} catch {}
|
|
1673
|
+
}
|
|
1674
|
+
const drift = checkProfileDrift(sessionId);
|
|
1675
|
+
if (drift) {
|
|
1676
|
+
process.stderr.write(`[kavachos] PROFILE DRIFT DETECTED for session ${sessionId}
|
|
1677
|
+
`);
|
|
1678
|
+
sealKernelViolation({
|
|
1679
|
+
session_id: sessionId,
|
|
1680
|
+
agent_id: opts.agentId ?? null,
|
|
1681
|
+
event_type: "PROFILE_DRIFT",
|
|
1682
|
+
violation_details: `Hash mismatch: stored=${drift.stored_hash.slice(0, 16)}... actual=${drift.actual_hash.slice(0, 16)}...`,
|
|
1683
|
+
severity: "CRITICAL"
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
resolve(result);
|
|
1687
|
+
});
|
|
1688
|
+
child.on("error", (err) => reject(err));
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function generateOnly(trustMask, domain, agentType) {
|
|
1692
|
+
const result = generateSeccompProfile(trustMask, domain, agentType);
|
|
1693
|
+
const falcoRules = generateFalcoRules(domain, trustMask);
|
|
1694
|
+
return { ...result, falcoRules };
|
|
1695
|
+
}
|
|
1696
|
+
var APPLY_SECCOMP_PY, KAVACHOS_DIR;
|
|
1697
|
+
var init_runner = __esm(() => {
|
|
1698
|
+
init_seccomp_profile_generator();
|
|
1699
|
+
init_falco_rule_generator();
|
|
1700
|
+
init_profile_store();
|
|
1701
|
+
init_kernel_receipt();
|
|
1702
|
+
init_egress_policy();
|
|
1703
|
+
init_config();
|
|
1704
|
+
APPLY_SECCOMP_PY = join2(dirname(new URL(import.meta.url).pathname), "apply-seccomp.py");
|
|
1705
|
+
KAVACHOS_DIR = join2(getAegisDir(), "kernel");
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
// ../../src/kernel/audit.ts
|
|
1709
|
+
var exports_audit = {};
|
|
1710
|
+
__export(exports_audit, {
|
|
1711
|
+
printAudit: () => printAudit,
|
|
1712
|
+
listSessions: () => listSessions,
|
|
1713
|
+
auditSession: () => auditSession
|
|
1714
|
+
});
|
|
1715
|
+
function auditSession(sessionId) {
|
|
1716
|
+
const profile = getProfile(sessionId);
|
|
1717
|
+
if (!profile) {
|
|
1718
|
+
return {
|
|
1719
|
+
session_id: sessionId,
|
|
1720
|
+
profile_found: false,
|
|
1721
|
+
profile_drift: false,
|
|
1722
|
+
receipt_count: 0,
|
|
1723
|
+
chain_valid: false,
|
|
1724
|
+
eu_ai_act_eligible: false,
|
|
1725
|
+
verdict: "NO_SESSION",
|
|
1726
|
+
summary: `Session ${sessionId} not found in kernel_profiles table. Either it was not launched via kavachos run, or records were cleaned up.`
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
const drift = checkProfileDrift(sessionId);
|
|
1730
|
+
const chainVerification = verifyReceiptChain(sessionId);
|
|
1731
|
+
const euEligible = !drift && chainVerification.valid;
|
|
1732
|
+
let verdict = "CLEAN";
|
|
1733
|
+
if (drift)
|
|
1734
|
+
verdict = "PROFILE_DRIFT";
|
|
1735
|
+
else if (!chainVerification.valid)
|
|
1736
|
+
verdict = "CHAIN_BROKEN";
|
|
1737
|
+
const verdictIcon = verdict === "CLEAN" ? "\u2705" : "\u274C";
|
|
1738
|
+
const summary = [
|
|
1739
|
+
`${verdictIcon} Session: ${sessionId}`,
|
|
1740
|
+
` Profile hash: ${profile.profile_hash.slice(0, 16)}... (${drift ? "DRIFTED" : "verified"})`,
|
|
1741
|
+
` Trust mask: 0x${profile.trust_mask.toString(16).padStart(8, "0")}`,
|
|
1742
|
+
` Domain: ${profile.domain}`,
|
|
1743
|
+
` Syscalls: ${profile.syscall_count}`,
|
|
1744
|
+
` Receipts: ${chainVerification.receipt_count} (chain ${chainVerification.valid ? "\u2705 unbroken" : "\u274C BROKEN at " + chainVerification.gap_at})`,
|
|
1745
|
+
` EU AI Act \xA714: ${euEligible ? "\u2705 eligible" : "\u274C NOT eligible"}`,
|
|
1746
|
+
` Verdict: ${verdict}`
|
|
1747
|
+
].join(`
|
|
1748
|
+
`);
|
|
1749
|
+
return {
|
|
1750
|
+
session_id: sessionId,
|
|
1751
|
+
profile_found: true,
|
|
1752
|
+
profile_hash: profile.profile_hash,
|
|
1753
|
+
profile_drift: !!drift,
|
|
1754
|
+
receipt_count: chainVerification.receipt_count,
|
|
1755
|
+
chain_valid: chainVerification.valid,
|
|
1756
|
+
chain_gap_at: chainVerification.gap_at,
|
|
1757
|
+
eu_ai_act_eligible: euEligible,
|
|
1758
|
+
verdict,
|
|
1759
|
+
summary
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
function listSessions() {
|
|
1763
|
+
const { getDb: getDb2 } = (init_db(), __toCommonJS(exports_db));
|
|
1764
|
+
const { ensureKernelSchema: ensureKernelSchema2 } = (init_profile_store(), __toCommonJS(exports_profile_store));
|
|
1765
|
+
ensureKernelSchema2();
|
|
1766
|
+
const db = getDb2();
|
|
1767
|
+
return db.query("SELECT session_id, domain, trust_mask, stored_at, syscall_count FROM kernel_profiles ORDER BY stored_at DESC LIMIT 50").all();
|
|
1768
|
+
}
|
|
1769
|
+
function printAudit(result) {
|
|
1770
|
+
console.log(result.summary);
|
|
1771
|
+
if (result.verdict !== "CLEAN") {
|
|
1772
|
+
console.log(`
|
|
1773
|
+
ACTION REQUIRED:`);
|
|
1774
|
+
if (result.profile_drift) {
|
|
1775
|
+
console.log(" \u2022 Profile drift detected \u2014 this session is a security incident (KOS-012)");
|
|
1776
|
+
console.log(" \u2022 Emit kavach.kernel.violation.detected and quarantine session");
|
|
1777
|
+
}
|
|
1778
|
+
if (!result.chain_valid) {
|
|
1779
|
+
console.log(` \u2022 Receipt chain broken at ${result.chain_gap_at} \u2014 tampered or missing evidence (INF-KOS-003)`);
|
|
1780
|
+
console.log(" \u2022 Session cannot be used as EU AI Act Article 14/15 evidence");
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
var init_audit = __esm(() => {
|
|
1785
|
+
init_profile_store();
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
// ../../src/kavach/perm-mask.ts
|
|
1789
|
+
var PERM, PERM_READ_ONLY, PERM_STANDARD, PERM_PRIVILEGED, PERM_ADMIN, BIT_NAMES;
|
|
1790
|
+
var init_perm_mask = __esm(() => {
|
|
1791
|
+
PERM = {
|
|
1792
|
+
READ: 1,
|
|
1793
|
+
WRITE: 2,
|
|
1794
|
+
EXEC_BASH: 4,
|
|
1795
|
+
SPAWN_AGENTS: 8,
|
|
1796
|
+
NETWORK: 16,
|
|
1797
|
+
DB_WRITE: 32,
|
|
1798
|
+
DB_READ: 64,
|
|
1799
|
+
DB_SCHEMA: 128,
|
|
1800
|
+
FS_CREATE: 256,
|
|
1801
|
+
FS_DELETE: 512,
|
|
1802
|
+
SERVICE_OP: 1024,
|
|
1803
|
+
SECRET_READ: 2048,
|
|
1804
|
+
CONFIG_WRITE: 4096,
|
|
1805
|
+
GIT_WRITE: 8192,
|
|
1806
|
+
EXTERNAL_API: 16384,
|
|
1807
|
+
PRIVILEGED: 32768,
|
|
1808
|
+
POLICY_ADMIN: 65536,
|
|
1809
|
+
MASK_ADMIN: 131072,
|
|
1810
|
+
AUDIT_WRITE: 262144,
|
|
1811
|
+
CROSS_SVC: 524288,
|
|
1812
|
+
PRODUCTION: 1048576
|
|
1813
|
+
};
|
|
1814
|
+
PERM_READ_ONLY = PERM.READ | PERM.DB_READ;
|
|
1815
|
+
PERM_STANDARD = PERM.READ | PERM.WRITE | PERM.EXEC_BASH | PERM.DB_READ | PERM.DB_WRITE | PERM.FS_CREATE | PERM.NETWORK | PERM.GIT_WRITE | PERM.EXTERNAL_API;
|
|
1816
|
+
PERM_PRIVILEGED = PERM_STANDARD | PERM.DB_SCHEMA | PERM.FS_DELETE | PERM.SERVICE_OP | PERM.CONFIG_WRITE | PERM.PRIVILEGED;
|
|
1817
|
+
PERM_ADMIN = PERM_PRIVILEGED | PERM.SECRET_READ | PERM.SPAWN_AGENTS | PERM.POLICY_ADMIN | PERM.MASK_ADMIN | PERM.AUDIT_WRITE | PERM.CROSS_SVC | PERM.PRODUCTION;
|
|
1818
|
+
BIT_NAMES = Object.entries(PERM).map(([name, bit]) => [bit, name]);
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
// ../../src/kavach/class-mask.ts
|
|
1822
|
+
var CLASS, CLASS_DEV_ONLY, CLASS_STANDARD, CLASS_PRIVILEGED, CLASS_NAMES;
|
|
1823
|
+
var init_class_mask = __esm(() => {
|
|
1824
|
+
CLASS = {
|
|
1825
|
+
DEV: 1,
|
|
1826
|
+
DEMO: 2,
|
|
1827
|
+
PROD: 4,
|
|
1828
|
+
SECRET: 8,
|
|
1829
|
+
MARITIME: 16,
|
|
1830
|
+
FINANCIAL: 32,
|
|
1831
|
+
PERSONAL: 64,
|
|
1832
|
+
INFRA: 128,
|
|
1833
|
+
ANKR_INTERNAL: 256
|
|
1834
|
+
};
|
|
1835
|
+
CLASS_DEV_ONLY = CLASS.DEV;
|
|
1836
|
+
CLASS_STANDARD = CLASS.DEV | CLASS.DEMO | CLASS.INFRA | CLASS.ANKR_INTERNAL;
|
|
1837
|
+
CLASS_PRIVILEGED = CLASS.DEV | CLASS.DEMO | CLASS.PROD | CLASS.INFRA | CLASS.ANKR_INTERNAL | CLASS.MARITIME;
|
|
1838
|
+
CLASS_NAMES = Object.entries(CLASS).map(([name, bit]) => [bit, name]);
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
// ../../src/sandbox/quarantine.ts
|
|
1842
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync } from "fs";
|
|
1843
|
+
import { join as join3 } from "path";
|
|
1844
|
+
function tryDbSync(record) {
|
|
1845
|
+
try {
|
|
1846
|
+
const { upsertAgent: upsertAgent2 } = (init_db(), __toCommonJS(exports_db));
|
|
1847
|
+
upsertAgent2({
|
|
1848
|
+
...record,
|
|
1849
|
+
budget_pool_reserved: 0,
|
|
1850
|
+
stop_requested: 0
|
|
1851
|
+
});
|
|
1852
|
+
} catch {}
|
|
1853
|
+
}
|
|
1854
|
+
function getAgentsDir() {
|
|
1855
|
+
const dir = join3(getAegisDir(), "agents");
|
|
1856
|
+
if (!existsSync3(dir))
|
|
1857
|
+
mkdirSync3(dir, { recursive: true });
|
|
1858
|
+
return dir;
|
|
1859
|
+
}
|
|
1860
|
+
function getStatePath(agentId) {
|
|
1861
|
+
return join3(getAgentsDir(), `${agentId}.state.json`);
|
|
1862
|
+
}
|
|
1863
|
+
function loadAgent(agentId) {
|
|
1864
|
+
const path = getStatePath(agentId);
|
|
1865
|
+
if (!existsSync3(path))
|
|
1866
|
+
return null;
|
|
1867
|
+
try {
|
|
1868
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
1869
|
+
} catch {
|
|
1870
|
+
return null;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
function saveAgent(record) {
|
|
1874
|
+
writeFileSync3(getStatePath(record.agent_id), JSON.stringify(record, null, 2));
|
|
1875
|
+
tryDbSync(record);
|
|
1876
|
+
}
|
|
1877
|
+
function transitionState(agentId, targetState, meta = {}) {
|
|
1878
|
+
const record = loadAgent(agentId);
|
|
1879
|
+
if (!record) {
|
|
1880
|
+
return { success: false, error: `Agent ${agentId} not found`, record: {} };
|
|
1881
|
+
}
|
|
1882
|
+
const allowed = VALID_TRANSITIONS[record.state];
|
|
1883
|
+
if (!allowed.includes(targetState)) {
|
|
1884
|
+
return {
|
|
1885
|
+
success: false,
|
|
1886
|
+
error: `Invalid transition ${record.state} \u2192 ${targetState} (allowed: ${allowed.join(", ") || "none"})`,
|
|
1887
|
+
record
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
record.state = targetState;
|
|
1891
|
+
record.last_seen = new Date().toISOString();
|
|
1892
|
+
if (targetState === "QUARANTINED") {
|
|
1893
|
+
record.quarantine_reason = meta.reason ?? null;
|
|
1894
|
+
record.quarantine_rule = meta.rule ?? null;
|
|
1895
|
+
}
|
|
1896
|
+
if (targetState === "RUNNING" && record.quarantine_reason) {
|
|
1897
|
+
record.release_reason = meta.reason ?? null;
|
|
1898
|
+
record.released_by = meta.released_by ?? null;
|
|
1899
|
+
record.quarantine_reason = null;
|
|
1900
|
+
record.quarantine_rule = null;
|
|
1901
|
+
}
|
|
1902
|
+
if (meta.resume_manifest_path) {
|
|
1903
|
+
record.resume_manifest_path = meta.resume_manifest_path;
|
|
1904
|
+
}
|
|
1905
|
+
saveAgent(record);
|
|
1906
|
+
return { success: true, record };
|
|
1907
|
+
}
|
|
1908
|
+
var VALID_TRANSITIONS;
|
|
1909
|
+
var init_quarantine = __esm(() => {
|
|
1910
|
+
init_config();
|
|
1911
|
+
VALID_TRANSITIONS = {
|
|
1912
|
+
RUNNING: ["QUARANTINED", "FORCE_CLOSED", "KILLED", "COMPLETED", "ZOMBIE"],
|
|
1913
|
+
QUARANTINED: ["RUNNING", "FORCE_CLOSED", "KILLED"],
|
|
1914
|
+
ORPHAN: ["FORCE_CLOSED", "KILLED", "COMPLETED"],
|
|
1915
|
+
ZOMBIE: ["FORCE_CLOSED", "KILLED"],
|
|
1916
|
+
FORCE_CLOSED: [],
|
|
1917
|
+
KILLED: [],
|
|
1918
|
+
COMPLETED: []
|
|
1919
|
+
};
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
// ../../src/kernel/kernel-notifier.ts
|
|
1923
|
+
var exports_kernel_notifier = {};
|
|
1924
|
+
__export(exports_kernel_notifier, {
|
|
1925
|
+
syscallToPlain: () => syscallToPlain,
|
|
1926
|
+
requestKernelApproval: () => requestKernelApproval,
|
|
1927
|
+
notifyAutoBlock: () => notifyAutoBlock,
|
|
1928
|
+
notifyAdvisory: () => notifyAdvisory
|
|
1929
|
+
});
|
|
1930
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1931
|
+
function syscallToPlain(syscall) {
|
|
1932
|
+
return SYSCALL_PLAIN[syscall] ?? `call syscall "${syscall}"`;
|
|
1933
|
+
}
|
|
1934
|
+
function buildMessage(event, approvalId) {
|
|
1935
|
+
const emoji = TIER_EMOJI[event.tier];
|
|
1936
|
+
const tierLabel = event.tier === 3 ? "Action Required" : event.tier === 4 ? "Auto-Blocked" : "Advisory";
|
|
1937
|
+
const lines = [
|
|
1938
|
+
`${emoji} KavachOS \u2014 ${tierLabel}`,
|
|
1939
|
+
``,
|
|
1940
|
+
`Agent: ${event.agent_id ?? "unknown"} | Domain: ${event.domain}`,
|
|
1941
|
+
`Session: ${event.session_id}`,
|
|
1942
|
+
``,
|
|
1943
|
+
`What happened:`,
|
|
1944
|
+
event.plain_summary,
|
|
1945
|
+
``,
|
|
1946
|
+
`Technical detail:`,
|
|
1947
|
+
event.technical_detail
|
|
1948
|
+
];
|
|
1949
|
+
if (event.falco_rule) {
|
|
1950
|
+
lines.push(`Falco rule: "${event.falco_rule}"`);
|
|
1951
|
+
}
|
|
1952
|
+
if (event.trust_mask !== undefined) {
|
|
1953
|
+
lines.push(`Trust mask: 0x${event.trust_mask.toString(16).padStart(8, "0")}`);
|
|
1954
|
+
}
|
|
1955
|
+
if (event.profile_hash) {
|
|
1956
|
+
lines.push(`Profile: ${event.profile_hash.slice(0, 12)}...`);
|
|
1957
|
+
}
|
|
1958
|
+
lines.push(``);
|
|
1959
|
+
lines.push(`Trigger: ${TRIGGER_LABEL[event.trigger]}`);
|
|
1960
|
+
if (event.tier === 3 && approvalId) {
|
|
1961
|
+
lines.push(``);
|
|
1962
|
+
lines.push(`Reply with one word:`);
|
|
1963
|
+
lines.push(` ALLOW ${approvalId} \u2014 permit, restore gate valve to OPEN`);
|
|
1964
|
+
lines.push(` STOP ${approvalId} \u2014 block, keep valve in current state`);
|
|
1965
|
+
lines.push(``);
|
|
1966
|
+
lines.push(`Expires: 10 min (silence = STOP)`);
|
|
1967
|
+
} else if (event.tier === 4) {
|
|
1968
|
+
lines.push(``);
|
|
1969
|
+
lines.push(`Agent is already blocked. No action needed.`);
|
|
1970
|
+
lines.push(`To release: kavachos valve release ${event.agent_id ?? event.session_id}`);
|
|
1971
|
+
} else if (event.tier === 2) {
|
|
1972
|
+
lines.push(``);
|
|
1973
|
+
lines.push(`No action needed \u2014 this is for your awareness.`);
|
|
1974
|
+
lines.push(`To review: kavachos profile show ${event.agent_id ?? event.session_id}`);
|
|
1975
|
+
}
|
|
1976
|
+
return lines.join(`
|
|
1977
|
+
`);
|
|
1978
|
+
}
|
|
1979
|
+
async function sendViaWebhook(message, approvalId) {
|
|
1980
|
+
const config = loadConfig();
|
|
1981
|
+
const kc = config.kavach;
|
|
1982
|
+
if (!kc?.enabled)
|
|
1983
|
+
return false;
|
|
1984
|
+
const webhookUrl = kc.webhook_url || kc.ankrclaw_url || "";
|
|
1985
|
+
if (!webhookUrl)
|
|
1986
|
+
return false;
|
|
1987
|
+
const channel = kc.notify_channel || "telegram";
|
|
1988
|
+
const to = channel === "telegram" ? kc.notify_telegram_chat_id : kc.notify_phone;
|
|
1989
|
+
if (!to)
|
|
1990
|
+
return false;
|
|
1991
|
+
try {
|
|
1992
|
+
const res = await fetch(`${webhookUrl}/api/notify`, {
|
|
1993
|
+
method: "POST",
|
|
1994
|
+
headers: { "Content-Type": "application/json" },
|
|
1995
|
+
body: JSON.stringify({
|
|
1996
|
+
to,
|
|
1997
|
+
message,
|
|
1998
|
+
service: "KAVACHOS",
|
|
1999
|
+
channel,
|
|
2000
|
+
approval_id: approvalId ?? undefined
|
|
2001
|
+
})
|
|
2002
|
+
});
|
|
2003
|
+
return res.ok;
|
|
2004
|
+
} catch {
|
|
2005
|
+
process.stderr.write(`[kavachos:notify] webhook unreachable \u2014 notification skipped
|
|
2006
|
+
`);
|
|
2007
|
+
return false;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
async function pollForKernelDecision(approvalId, timeoutMs = 600000) {
|
|
2011
|
+
const deadline = Date.now() + timeoutMs;
|
|
2012
|
+
const POLL_INTERVAL = 2000;
|
|
2013
|
+
process.stderr.write(`[kavachos:notify] Waiting for kernel decision ${approvalId} (${Math.round(timeoutMs / 1000)}s timeout)
|
|
2014
|
+
`);
|
|
2015
|
+
while (Date.now() < deadline) {
|
|
2016
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
2017
|
+
const approval = getKavachApproval(approvalId);
|
|
2018
|
+
if (!approval)
|
|
2019
|
+
continue;
|
|
2020
|
+
if (approval.status === "allowed")
|
|
2021
|
+
return "ALLOW";
|
|
2022
|
+
if (approval.status === "stopped" || approval.status === "timed_out")
|
|
2023
|
+
return "STOP";
|
|
2024
|
+
}
|
|
2025
|
+
process.stderr.write(`[kavachos:notify] Decision timeout for ${approvalId} \u2192 STOP
|
|
2026
|
+
`);
|
|
2027
|
+
return "STOP";
|
|
2028
|
+
}
|
|
2029
|
+
async function notifyAdvisory(event) {
|
|
2030
|
+
const message = buildMessage(event, null);
|
|
2031
|
+
process.stderr.write(`[kavachos:notify] ADVISORY: ${event.plain_summary}
|
|
2032
|
+
`);
|
|
2033
|
+
sendViaWebhook(message, null).catch(() => {});
|
|
2034
|
+
}
|
|
2035
|
+
async function requestKernelApproval(event) {
|
|
2036
|
+
const approvalId = `KOS-${randomBytes3(4).toString("hex").toUpperCase()}`;
|
|
2037
|
+
createKavachApproval({
|
|
2038
|
+
id: approvalId,
|
|
2039
|
+
created_at: new Date().toISOString(),
|
|
2040
|
+
command: event.technical_detail,
|
|
2041
|
+
tool_name: `kavachos:${event.trigger}`,
|
|
2042
|
+
level: 3,
|
|
2043
|
+
consequence: event.plain_summary,
|
|
2044
|
+
session_id: event.session_id,
|
|
2045
|
+
timeout_ms: 600000
|
|
2046
|
+
});
|
|
2047
|
+
const message = buildMessage(event, approvalId);
|
|
2048
|
+
process.stderr.write(`[kavachos:notify] TIER-3 alert sent \u2014 ${approvalId}: ${event.plain_summary}
|
|
2049
|
+
`);
|
|
2050
|
+
await sendViaWebhook(message, approvalId);
|
|
2051
|
+
return pollForKernelDecision(approvalId);
|
|
2052
|
+
}
|
|
2053
|
+
async function notifyAutoBlock(event) {
|
|
2054
|
+
const message = buildMessage(event, null);
|
|
2055
|
+
process.stderr.write(`[kavachos:notify] AUTO-BLOCK: ${event.plain_summary}
|
|
2056
|
+
`);
|
|
2057
|
+
sendViaWebhook(message, null).catch(() => {});
|
|
2058
|
+
}
|
|
2059
|
+
var SYSCALL_PLAIN, TIER_EMOJI, TRIGGER_LABEL;
|
|
2060
|
+
var init_kernel_notifier = __esm(() => {
|
|
2061
|
+
init_config();
|
|
2062
|
+
init_db();
|
|
2063
|
+
SYSCALL_PLAIN = {
|
|
2064
|
+
execve: "execute a program or shell command",
|
|
2065
|
+
execveat: "execute a program from a file descriptor",
|
|
2066
|
+
clone: "create a child process",
|
|
2067
|
+
clone3: "create a child process (modern)",
|
|
2068
|
+
fork: "duplicate the current process",
|
|
2069
|
+
socket: "open a network socket",
|
|
2070
|
+
connect: "connect to a remote server",
|
|
2071
|
+
bind: "listen on a network port",
|
|
2072
|
+
sendto: "send data over the network",
|
|
2073
|
+
recvfrom: "receive data from the network",
|
|
2074
|
+
open: "open a file",
|
|
2075
|
+
openat: "open a file (relative path)",
|
|
2076
|
+
unlink: "delete a file",
|
|
2077
|
+
unlinkat: "delete a file (relative path)",
|
|
2078
|
+
rename: "rename or move a file",
|
|
2079
|
+
chmod: "change file permissions",
|
|
2080
|
+
chown: "change file ownership",
|
|
2081
|
+
ptrace: "attach to and control another process",
|
|
2082
|
+
mmap: "map memory (large allocation)",
|
|
2083
|
+
memfd_create: "create an anonymous memory region",
|
|
2084
|
+
inotify_init1: "watch a directory for file changes",
|
|
2085
|
+
keyctl: "access cryptographic keys",
|
|
2086
|
+
sethostname: "change the system hostname",
|
|
2087
|
+
prctl: "change process control settings",
|
|
2088
|
+
kill: "send a signal to another process",
|
|
2089
|
+
tgkill: "send a signal to a specific thread",
|
|
2090
|
+
seccomp: "modify syscall filtering (privilege escalation attempt)",
|
|
2091
|
+
mount: "mount a filesystem",
|
|
2092
|
+
umount2: "unmount a filesystem",
|
|
2093
|
+
chroot: "change the root directory",
|
|
2094
|
+
pivot_root: "change the root filesystem",
|
|
2095
|
+
setuid: "change user identity",
|
|
2096
|
+
setgid: "change group identity",
|
|
2097
|
+
capset: "modify Linux capabilities"
|
|
2098
|
+
};
|
|
2099
|
+
TIER_EMOJI = {
|
|
2100
|
+
1: "\u2139\uFE0F",
|
|
2101
|
+
2: "\uD83D\uDFE1",
|
|
2102
|
+
3: "\uD83D\uDD34",
|
|
2103
|
+
4: "\uD83D\uDED1"
|
|
2104
|
+
};
|
|
2105
|
+
TRIGGER_LABEL = {
|
|
2106
|
+
falco_critical: "Falco CRITICAL \u2014 suspicious kernel event",
|
|
2107
|
+
valve_cracked: "Gate valve \u2192 CRACKED (3+ violations)",
|
|
2108
|
+
valve_locked: "Gate valve \u2192 LOCKED (agent fully stopped)",
|
|
2109
|
+
rate_exceeded: "Violation rate exceeded (5+/min \u2014 possible exfil)",
|
|
2110
|
+
supervisor_ambiguous: "Supervisor cannot auto-decide",
|
|
2111
|
+
profile_drift: "Seccomp profile tampering detected"
|
|
2112
|
+
};
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
// ../../src/kavach/gate-valve.ts
|
|
2116
|
+
var exports_gate_valve = {};
|
|
2117
|
+
__export(exports_gate_valve, {
|
|
2118
|
+
throttleValve: () => throttleValve,
|
|
2119
|
+
recordViolation: () => recordViolation,
|
|
2120
|
+
readValve: () => readValve,
|
|
2121
|
+
openValve: () => openValve,
|
|
2122
|
+
lockValve: () => lockValve,
|
|
2123
|
+
initValve: () => initValve,
|
|
2124
|
+
incrementLoopCount: () => incrementLoopCount,
|
|
2125
|
+
crackValve: () => crackValve,
|
|
2126
|
+
closeValve: () => closeValve,
|
|
2127
|
+
checkValve: () => checkValve
|
|
2128
|
+
});
|
|
2129
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
2130
|
+
import { join as join4 } from "path";
|
|
2131
|
+
function getValveDir() {
|
|
2132
|
+
const dir = join4(getAegisDir(), "agents");
|
|
2133
|
+
if (!existsSync4(dir))
|
|
2134
|
+
mkdirSync4(dir, { recursive: true });
|
|
2135
|
+
return dir;
|
|
2136
|
+
}
|
|
2137
|
+
function valvePath(agentId) {
|
|
2138
|
+
return join4(getValveDir(), `${agentId}.valve.json`);
|
|
2139
|
+
}
|
|
2140
|
+
function readValve(agentId) {
|
|
2141
|
+
const path = valvePath(agentId);
|
|
2142
|
+
if (!existsSync4(path)) {
|
|
2143
|
+
const defaultRecord = {
|
|
2144
|
+
agent_id: agentId,
|
|
2145
|
+
state: "OPEN",
|
|
2146
|
+
declared_perm_mask: PERM_STANDARD,
|
|
2147
|
+
effective_perm_mask: PERM_STANDARD,
|
|
2148
|
+
declared_class_mask: CLASS_STANDARD,
|
|
2149
|
+
effective_class_mask: CLASS_STANDARD,
|
|
2150
|
+
violation_count: 0,
|
|
2151
|
+
loop_count: 0,
|
|
2152
|
+
narrowed_at: null,
|
|
2153
|
+
narrowed_reason: null,
|
|
2154
|
+
locked_by: null,
|
|
2155
|
+
locked_at: null,
|
|
2156
|
+
quarantine_flag: false
|
|
2157
|
+
};
|
|
2158
|
+
writeValve(defaultRecord);
|
|
2159
|
+
return defaultRecord;
|
|
2160
|
+
}
|
|
2161
|
+
try {
|
|
2162
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
2163
|
+
} catch {
|
|
2164
|
+
return readValve(agentId);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
function writeValve(record) {
|
|
2168
|
+
writeFileSync4(valvePath(record.agent_id), JSON.stringify(record, null, 2));
|
|
2169
|
+
}
|
|
2170
|
+
function initValve(agentId, declaredPermMask, declaredClassMask) {
|
|
2171
|
+
const record = {
|
|
2172
|
+
agent_id: agentId,
|
|
2173
|
+
state: "OPEN",
|
|
2174
|
+
declared_perm_mask: declaredPermMask,
|
|
2175
|
+
effective_perm_mask: declaredPermMask,
|
|
2176
|
+
declared_class_mask: declaredClassMask,
|
|
2177
|
+
effective_class_mask: declaredClassMask,
|
|
2178
|
+
violation_count: 0,
|
|
2179
|
+
loop_count: 0,
|
|
2180
|
+
narrowed_at: null,
|
|
2181
|
+
narrowed_reason: null,
|
|
2182
|
+
locked_by: null,
|
|
2183
|
+
locked_at: null,
|
|
2184
|
+
quarantine_flag: false
|
|
2185
|
+
};
|
|
2186
|
+
writeValve(record);
|
|
2187
|
+
return record;
|
|
2188
|
+
}
|
|
2189
|
+
function recordViolation(agentId, reason) {
|
|
2190
|
+
const record = readValve(agentId);
|
|
2191
|
+
record.violation_count++;
|
|
2192
|
+
if (record.violation_count === 1 && record.state === "OPEN") {
|
|
2193
|
+
return throttleValve(agentId, `auto-throttle: first violation \u2014 ${reason}`);
|
|
2194
|
+
}
|
|
2195
|
+
if (record.violation_count >= 3 && record.state === "THROTTLED") {
|
|
2196
|
+
return crackValve(agentId, `auto-crack: ${record.violation_count} violations \u2014 ${reason}`);
|
|
2197
|
+
}
|
|
2198
|
+
writeValve(record);
|
|
2199
|
+
return record;
|
|
2200
|
+
}
|
|
2201
|
+
function incrementLoopCount(agentId) {
|
|
2202
|
+
const record = readValve(agentId);
|
|
2203
|
+
record.loop_count++;
|
|
2204
|
+
if (record.loop_count > 100 && record.violation_count > 0 && record.state !== "CLOSED" && record.state !== "LOCKED") {
|
|
2205
|
+
return closeValve(agentId, `auto-close: runaway agent (loop=${record.loop_count}, violations=${record.violation_count})`);
|
|
2206
|
+
}
|
|
2207
|
+
writeValve(record);
|
|
2208
|
+
return record;
|
|
2209
|
+
}
|
|
2210
|
+
function throttleValve(agentId, reason) {
|
|
2211
|
+
const record = readValve(agentId);
|
|
2212
|
+
if (record.state === "CLOSED" || record.state === "LOCKED")
|
|
2213
|
+
return record;
|
|
2214
|
+
record.state = "THROTTLED";
|
|
2215
|
+
record.effective_perm_mask &= ~PERM.SPAWN_AGENTS;
|
|
2216
|
+
record.narrowed_at = new Date().toISOString();
|
|
2217
|
+
record.narrowed_reason = reason;
|
|
2218
|
+
writeValve(record);
|
|
2219
|
+
process.stderr.write(`[KAVACH:valve] ${agentId} \u2192 THROTTLED \u2014 ${reason}
|
|
2220
|
+
`);
|
|
2221
|
+
return record;
|
|
2222
|
+
}
|
|
2223
|
+
function crackValve(agentId, reason) {
|
|
2224
|
+
const record = readValve(agentId);
|
|
2225
|
+
if (record.state === "CLOSED" || record.state === "LOCKED")
|
|
2226
|
+
return record;
|
|
2227
|
+
record.state = "CRACKED";
|
|
2228
|
+
record.effective_perm_mask &= ~(PERM.SPAWN_AGENTS | PERM.EXEC_BASH);
|
|
2229
|
+
record.narrowed_at = new Date().toISOString();
|
|
2230
|
+
record.narrowed_reason = reason;
|
|
2231
|
+
writeValve(record);
|
|
2232
|
+
process.stderr.write(`[KAVACH:valve] ${agentId} \u2192 CRACKED \u2014 ${reason}
|
|
2233
|
+
`);
|
|
2234
|
+
Promise.resolve().then(() => (init_kernel_notifier(), exports_kernel_notifier)).then(({ requestKernelApproval: requestKernelApproval2 }) => {
|
|
2235
|
+
requestKernelApproval2({
|
|
2236
|
+
tier: 3,
|
|
2237
|
+
session_id: agentId,
|
|
2238
|
+
agent_id: agentId,
|
|
2239
|
+
domain: "unknown",
|
|
2240
|
+
trigger: "valve_cracked",
|
|
2241
|
+
plain_summary: `Agent ${agentId} has triggered 3 or more violations. It is now running in restricted mode (no new processes, no shell commands). Decide whether to let it continue or stop it.`,
|
|
2242
|
+
technical_detail: `Gate valve \u2192 CRACKED. Reason: ${reason}. SPAWN_AGENTS + EXEC_BASH bits cleared from effective_perm_mask.`
|
|
2243
|
+
}).then((decision) => {
|
|
2244
|
+
if (decision === "STOP") {
|
|
2245
|
+
closeValve(agentId, "Human STOP after CRACKED notification");
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
}).catch(() => {});
|
|
2249
|
+
return record;
|
|
2250
|
+
}
|
|
2251
|
+
function closeValve(agentId, reason) {
|
|
2252
|
+
const record = readValve(agentId);
|
|
2253
|
+
if (record.state === "LOCKED")
|
|
2254
|
+
return record;
|
|
2255
|
+
record.state = "CLOSED";
|
|
2256
|
+
record.effective_perm_mask = 0;
|
|
2257
|
+
record.effective_class_mask = 0;
|
|
2258
|
+
record.narrowed_at = new Date().toISOString();
|
|
2259
|
+
record.narrowed_reason = reason;
|
|
2260
|
+
writeValve(record);
|
|
2261
|
+
process.stderr.write(`[KAVACH:valve] ${agentId} \u2192 CLOSED \u2014 ${reason}
|
|
2262
|
+
`);
|
|
2263
|
+
return record;
|
|
2264
|
+
}
|
|
2265
|
+
function lockValve(agentId, reason, lockedBy = "system") {
|
|
2266
|
+
const record = readValve(agentId);
|
|
2267
|
+
record.state = "LOCKED";
|
|
2268
|
+
record.effective_perm_mask = 0;
|
|
2269
|
+
record.effective_class_mask = 0;
|
|
2270
|
+
record.narrowed_at = new Date().toISOString();
|
|
2271
|
+
record.narrowed_reason = reason;
|
|
2272
|
+
record.locked_by = lockedBy;
|
|
2273
|
+
record.locked_at = new Date().toISOString();
|
|
2274
|
+
record.quarantine_flag = true;
|
|
2275
|
+
writeValve(record);
|
|
2276
|
+
process.stderr.write(`[KAVACH:valve] ${agentId} \u2192 LOCKED by ${lockedBy} \u2014 ${reason}
|
|
2277
|
+
`);
|
|
2278
|
+
Promise.resolve().then(() => (init_kernel_notifier(), exports_kernel_notifier)).then(({ notifyAutoBlock: notifyAutoBlock2 }) => {
|
|
2279
|
+
notifyAutoBlock2({
|
|
2280
|
+
tier: 4,
|
|
2281
|
+
session_id: agentId,
|
|
2282
|
+
agent_id: agentId,
|
|
2283
|
+
domain: "unknown",
|
|
2284
|
+
trigger: "valve_locked",
|
|
2285
|
+
plain_summary: `Agent ${agentId} has been fully stopped and quarantined. It cannot perform any actions until a human releases it.`,
|
|
2286
|
+
technical_detail: `Gate valve \u2192 LOCKED by ${lockedBy}. Reason: ${reason}. effective_perm_mask=0.`
|
|
2287
|
+
});
|
|
2288
|
+
}).catch(() => {});
|
|
2289
|
+
try {
|
|
2290
|
+
const agentRecord = loadAgent(agentId);
|
|
2291
|
+
if (agentRecord && (agentRecord.state === "RUNNING" || agentRecord.state === "ORPHAN")) {
|
|
2292
|
+
transitionState(agentId, "QUARANTINED", { reason: `valve LOCKED: ${reason}`, rule: "KAV-066" });
|
|
2293
|
+
}
|
|
2294
|
+
} catch {}
|
|
2295
|
+
return record;
|
|
2296
|
+
}
|
|
2297
|
+
function openValve(agentId, releasedBy) {
|
|
2298
|
+
const record = readValve(agentId);
|
|
2299
|
+
if (record.state === "CLOSED" || record.state === "LOCKED") {
|
|
2300
|
+
throw new Error(`Cannot auto-open ${record.state} valve for ${agentId} \u2014 requires human via quarantine release`);
|
|
2301
|
+
}
|
|
2302
|
+
record.state = "OPEN";
|
|
2303
|
+
record.effective_perm_mask = record.declared_perm_mask;
|
|
2304
|
+
record.effective_class_mask = record.declared_class_mask;
|
|
2305
|
+
record.narrowed_at = null;
|
|
2306
|
+
record.narrowed_reason = null;
|
|
2307
|
+
writeValve(record);
|
|
2308
|
+
process.stderr.write(`[KAVACH:valve] ${agentId} \u2192 OPEN (released by ${releasedBy})
|
|
2309
|
+
`);
|
|
2310
|
+
return record;
|
|
2311
|
+
}
|
|
2312
|
+
function checkValve(agentId, requiredPermBits, resourceClassBits) {
|
|
2313
|
+
const record = readValve(agentId);
|
|
2314
|
+
if (record.state === "CLOSED" || record.state === "LOCKED") {
|
|
2315
|
+
if (record.state === "CLOSED") {
|
|
2316
|
+
lockValve(agentId, "closed agent attempted tool call", "auto-escalation");
|
|
2317
|
+
}
|
|
2318
|
+
return {
|
|
2319
|
+
allowed: false,
|
|
2320
|
+
level: 0,
|
|
2321
|
+
reason: `Agent is in ${record.state} state \u2014 all capabilities revoked`,
|
|
2322
|
+
rule: "KAV-063",
|
|
2323
|
+
valve_state: record.state
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
if (requiredPermBits !== 0 && (record.effective_perm_mask & requiredPermBits) !== requiredPermBits) {
|
|
2327
|
+
const missing = requiredPermBits & ~record.effective_perm_mask;
|
|
2328
|
+
recordViolation(agentId, `perm_mask missing bits 0x${missing.toString(16)}`);
|
|
2329
|
+
return {
|
|
2330
|
+
allowed: false,
|
|
2331
|
+
level: 0,
|
|
2332
|
+
reason: `perm_mask check failed \u2014 required 0x${requiredPermBits.toString(16)}, effective 0x${record.effective_perm_mask.toString(16)}`,
|
|
2333
|
+
rule: "KAV-061",
|
|
2334
|
+
valve_state: record.state
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
const effectiveClass = record.effective_class_mask === 0 ? 1 : record.effective_class_mask;
|
|
2338
|
+
if (resourceClassBits !== 0 && (effectiveClass & resourceClassBits) === 0) {
|
|
2339
|
+
recordViolation(agentId, `class_mask 0x${resourceClassBits.toString(16)} not granted`);
|
|
2340
|
+
return {
|
|
2341
|
+
allowed: false,
|
|
2342
|
+
level: 1,
|
|
2343
|
+
reason: `class_mask check failed \u2014 resource requires class 0x${resourceClassBits.toString(16)}, agent has 0x${effectiveClass.toString(16)}`,
|
|
2344
|
+
rule: "KAV-062",
|
|
2345
|
+
valve_state: record.state
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
return {
|
|
2349
|
+
allowed: true,
|
|
2350
|
+
level: 0,
|
|
2351
|
+
reason: "",
|
|
2352
|
+
rule: "",
|
|
2353
|
+
valve_state: record.state
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
var init_gate_valve = __esm(() => {
|
|
2357
|
+
init_config();
|
|
2358
|
+
init_perm_mask();
|
|
2359
|
+
init_class_mask();
|
|
2360
|
+
init_quarantine();
|
|
2361
|
+
});
|
|
2362
|
+
|
|
2363
|
+
// ../../src/kavachos-cli.ts
|
|
2364
|
+
var command = Bun.argv[2] || "help";
|
|
2365
|
+
var subCommand = Bun.argv[3];
|
|
2366
|
+
var args = Bun.argv.slice(3);
|
|
2367
|
+
async function main() {
|
|
2368
|
+
switch (command) {
|
|
2369
|
+
case "run":
|
|
2370
|
+
return cmdRun(args);
|
|
2371
|
+
case "generate":
|
|
2372
|
+
case "gen":
|
|
2373
|
+
return cmdGenerate(args);
|
|
2374
|
+
case "profile":
|
|
2375
|
+
if (subCommand === "show")
|
|
2376
|
+
return cmdProfileShow(Bun.argv.slice(4));
|
|
2377
|
+
console.error(`Unknown profile sub-command: ${subCommand}. Try: kavachos profile show <agent-id>`);
|
|
2378
|
+
process.exit(1);
|
|
2379
|
+
break;
|
|
2380
|
+
case "audit":
|
|
2381
|
+
return cmdAudit(args);
|
|
2382
|
+
case "rules":
|
|
2383
|
+
return cmdRules(args);
|
|
2384
|
+
case "init":
|
|
2385
|
+
return cmdInit(args);
|
|
2386
|
+
case "version":
|
|
2387
|
+
case "--version":
|
|
2388
|
+
case "-v":
|
|
2389
|
+
console.log("kavachos 2.0.0 (KavachOS KERNEL \u2014 xShieldAI Posture Suite)");
|
|
2390
|
+
console.log("AGPL-3.0 \xB7 DOI 10.5281/zenodo.19908430");
|
|
2391
|
+
break;
|
|
2392
|
+
case "help":
|
|
2393
|
+
case "--help":
|
|
2394
|
+
case "-h":
|
|
2395
|
+
printHelp();
|
|
2396
|
+
break;
|
|
2397
|
+
default:
|
|
2398
|
+
console.error(`Unknown command: ${command}`);
|
|
2399
|
+
printHelp();
|
|
2400
|
+
process.exit(1);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
async function cmdRun(args2) {
|
|
2404
|
+
const { runWithKernel: runWithKernel2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
|
|
2405
|
+
const opts = parseRunOpts(args2);
|
|
2406
|
+
const agentArgs = args2.filter((a) => !a.startsWith("--"));
|
|
2407
|
+
if (agentArgs.length === 0) {
|
|
2408
|
+
console.error("Usage: kavachos run <agent_binary> [args] [--trust-mask=N] [--domain=D] [--dry-run] [--verbose]");
|
|
2409
|
+
process.exit(1);
|
|
2410
|
+
}
|
|
2411
|
+
try {
|
|
2412
|
+
const result = await runWithKernel2(agentArgs, opts);
|
|
2413
|
+
if (opts.dryRun)
|
|
2414
|
+
return;
|
|
2415
|
+
if (result.exitCode !== 0)
|
|
2416
|
+
process.exit(result.exitCode);
|
|
2417
|
+
} catch (err) {
|
|
2418
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2419
|
+
console.error(`[kavachos] run failed: ${message}`);
|
|
2420
|
+
process.exit(1);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
async function cmdGenerate(args2) {
|
|
2424
|
+
const { generateOnly: generateOnly2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
|
|
2425
|
+
const { profileSummary: profileSummary2 } = await Promise.resolve().then(() => (init_seccomp_profile_generator(), exports_seccomp_profile_generator));
|
|
2426
|
+
const trustMask = parseTrustMask(args2);
|
|
2427
|
+
const domain = parseDomain(args2);
|
|
2428
|
+
const outFlag = args2.find((a) => a.startsWith("--out="))?.split("=")[1];
|
|
2429
|
+
const jsonFlag = args2.includes("--json");
|
|
2430
|
+
const agentType = args2.find((a) => a.startsWith("--agent-type="))?.split("=")[1] ?? "claude-code";
|
|
2431
|
+
const result = generateOnly2(trustMask, domain, agentType);
|
|
2432
|
+
if (jsonFlag) {
|
|
2433
|
+
console.log(JSON.stringify(result.profile, null, 2));
|
|
2434
|
+
} else {
|
|
2435
|
+
console.log(profileSummary2(result));
|
|
2436
|
+
console.log(`
|
|
2437
|
+
trust_mask: 0x${trustMask.toString(16).padStart(8, "0")}`);
|
|
2438
|
+
console.log(` syscalls: ${result.syscall_count}`);
|
|
2439
|
+
console.log(` hash: ${result.hash}`);
|
|
2440
|
+
console.log(`
|
|
2441
|
+
Falco rules: ${result.falcoRules.rule_count} rules for domain '${domain}'`);
|
|
2442
|
+
}
|
|
2443
|
+
if (outFlag) {
|
|
2444
|
+
const { writeFileSync: writeFileSync5 } = await import("fs");
|
|
2445
|
+
writeFileSync5(outFile(outFlag, "seccomp.json"), JSON.stringify(result.profile, null, 2));
|
|
2446
|
+
writeFileSync5(outFile(outFlag, "falco.yaml"), result.falcoRules.rules);
|
|
2447
|
+
console.log(`
|
|
2448
|
+
Written: ${outFile(outFlag, "seccomp.json")} + ${outFile(outFlag, "falco.yaml")}`);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
async function cmdAudit(args2) {
|
|
2452
|
+
const { auditSession: auditSession2, listSessions: listSessions2, printAudit: printAudit2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
2453
|
+
const listAll = args2.includes("--all") || args2.length === 0;
|
|
2454
|
+
if (listAll) {
|
|
2455
|
+
const sessions = listSessions2();
|
|
2456
|
+
if (sessions.length === 0) {
|
|
2457
|
+
console.log("No kernel-governed sessions found in aegis.db");
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
console.log(`
|
|
2461
|
+
KavachOS governed sessions (${sessions.length}):
|
|
2462
|
+
`);
|
|
2463
|
+
for (const s of sessions) {
|
|
2464
|
+
const result2 = auditSession2(s.session_id);
|
|
2465
|
+
const icon = result2.verdict === "CLEAN" ? "\u2705" : "\u274C";
|
|
2466
|
+
console.log(`${icon} ${s.session_id.padEnd(28)} domain=${s.domain.padEnd(12)} trust=0x${s.trust_mask.toString(16).padStart(8, "0")} syscalls=${s.syscall_count} chain=${result2.chain_valid ? "ok" : "BROKEN"}`);
|
|
2467
|
+
}
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
const sessionId = args2.find((a) => !a.startsWith("--")) ?? "";
|
|
2471
|
+
if (!sessionId) {
|
|
2472
|
+
console.error("Usage: kavachos audit <session-id> | --all");
|
|
2473
|
+
process.exit(1);
|
|
2474
|
+
}
|
|
2475
|
+
const result = auditSession2(sessionId);
|
|
2476
|
+
printAudit2(result);
|
|
2477
|
+
if (result.verdict !== "CLEAN")
|
|
2478
|
+
process.exit(1);
|
|
2479
|
+
}
|
|
2480
|
+
async function cmdRules(args2) {
|
|
2481
|
+
const { generateFalcoRules: generateFalcoRules2 } = await Promise.resolve().then(() => (init_falco_rule_generator(), exports_falco_rule_generator));
|
|
2482
|
+
const domain = parseDomain(args2);
|
|
2483
|
+
const trustMask = parseTrustMask(args2);
|
|
2484
|
+
const jsonFlag = args2.includes("--json");
|
|
2485
|
+
const rules = generateFalcoRules2(domain, trustMask);
|
|
2486
|
+
if (jsonFlag) {
|
|
2487
|
+
console.log(JSON.stringify(rules, null, 2));
|
|
2488
|
+
} else {
|
|
2489
|
+
console.log(`# KavachOS Falco Rules \u2014 domain=${domain} trust_mask=0x${trustMask.toString(16).padStart(8, "0")}`);
|
|
2490
|
+
console.log(`# Generated: ${rules.generated_at} | Rules: ${rules.rule_count}
|
|
2491
|
+
`);
|
|
2492
|
+
console.log(rules.rules);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
async function cmdProfileShow(args2) {
|
|
2496
|
+
const { getProfileForAgent: getProfileForAgent2 } = await Promise.resolve().then(() => (init_profile_store(), exports_profile_store));
|
|
2497
|
+
const { listSessions: listSessions2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
2498
|
+
const jsonFlag = args2.includes("--json");
|
|
2499
|
+
const sessionFlag = args2.find((a) => a.startsWith("--session="))?.split("=")[1] ?? args2[args2.indexOf("--session") + 1];
|
|
2500
|
+
const agentId = args2.find((a) => !a.startsWith("--")) ?? null;
|
|
2501
|
+
if (!agentId) {
|
|
2502
|
+
const sessions = listSessions2();
|
|
2503
|
+
if (sessions.length === 0) {
|
|
2504
|
+
console.log("No governed sessions found in aegis.db");
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
if (jsonFlag) {
|
|
2508
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
2509
|
+
} else {
|
|
2510
|
+
console.log(`
|
|
2511
|
+
KavachOS profiles (${sessions.length} sessions):
|
|
2512
|
+
`);
|
|
2513
|
+
for (const s of sessions) {
|
|
2514
|
+
console.log(` ${s.session_id.padEnd(28)} domain=${s.domain.padEnd(12)} trust=0x${s.trust_mask.toString(16).padStart(8, "0")} syscalls=${s.syscall_count} hash=${s.profile_hash?.slice(0, 12) ?? "n/a"}...`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
const profile = getProfileForAgent2(agentId, sessionFlag ?? null);
|
|
2520
|
+
if (!profile) {
|
|
2521
|
+
console.error(`No profile found for agent-id="${agentId}"${sessionFlag ? ` session="${sessionFlag}"` : ""}`);
|
|
2522
|
+
process.exit(1);
|
|
2523
|
+
}
|
|
2524
|
+
const { readValve: readValve2 } = await Promise.resolve().then(() => (init_gate_valve(), exports_gate_valve));
|
|
2525
|
+
const valveState = readValve2(agentId);
|
|
2526
|
+
if (jsonFlag) {
|
|
2527
|
+
console.log(JSON.stringify({ profile, valve_state: valveState }, null, 2));
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
const kv = profile._kavachos ?? {};
|
|
2531
|
+
console.log(`
|
|
2532
|
+
KavachOS Profile \u2014 agent: ${agentId}`);
|
|
2533
|
+
console.log(` session_id: ${profile._session_id ?? sessionFlag ?? "n/a"}`);
|
|
2534
|
+
console.log(` domain: ${kv.domain ?? "n/a"}`);
|
|
2535
|
+
console.log(` trust_mask: 0x${(kv.trust_mask ?? 0).toString(16).padStart(8, "0")}`);
|
|
2536
|
+
console.log(` syscalls: ${profile.syscalls?.[0]?.names?.length ?? 0}`);
|
|
2537
|
+
console.log(` k_seal: ${kv.k_seal ?? "n/a"}`);
|
|
2538
|
+
console.log(` generated_at: ${kv.generated_at ?? "n/a"}`);
|
|
2539
|
+
console.log(` default: ${profile.defaultAction}`);
|
|
2540
|
+
console.log(`
|
|
2541
|
+
Gate valve state:`);
|
|
2542
|
+
const icon = valveState.state === "OPEN" ? "\u2705" : valveState.state === "THROTTLED" ? "\u26A0\uFE0F" : valveState.state === "CRACKED" ? "\uD83D\uDFE0" : "\uD83D\uDD34";
|
|
2543
|
+
console.log(` ${icon} ${valveState.state} violations=${valveState.violation_count} reason="${valveState.narrowed_reason ?? "none"}"`);
|
|
2544
|
+
if (valveState.state !== "OPEN") {
|
|
2545
|
+
console.log(` narrowed_at: ${valveState.narrowed_at ?? "n/a"} locked_by: ${valveState.locked_by ?? "n/a"}`);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
async function cmdInit(args2) {
|
|
2549
|
+
const { existsSync: existsSync5, writeFileSync: writeFileSync5 } = await import("fs");
|
|
2550
|
+
const { resolve } = await import("path");
|
|
2551
|
+
const configPath = resolve(process.cwd(), ".kavachos.json");
|
|
2552
|
+
const force = args2.includes("--force");
|
|
2553
|
+
if (existsSync5(configPath) && !force) {
|
|
2554
|
+
console.error(`.kavachos.json already exists (use --force to overwrite): ${configPath}`);
|
|
2555
|
+
process.exit(1);
|
|
2556
|
+
}
|
|
2557
|
+
const domain = parseDomain(args2);
|
|
2558
|
+
const trustMask = parseTrustMask(args2);
|
|
2559
|
+
const agentType = args2.find((a) => a.startsWith("--agent-type="))?.split("=")[1] ?? "claude-code";
|
|
2560
|
+
const config = {
|
|
2561
|
+
kavachos_version: "2.0.0",
|
|
2562
|
+
schema: "kavachos-config-v1",
|
|
2563
|
+
project: {
|
|
2564
|
+
name: resolve(process.cwd()).split("/").pop() ?? "unnamed",
|
|
2565
|
+
domain
|
|
2566
|
+
},
|
|
2567
|
+
agents: [
|
|
2568
|
+
{
|
|
2569
|
+
id: agentType,
|
|
2570
|
+
type: agentType,
|
|
2571
|
+
trust_mask: trustMask,
|
|
2572
|
+
domain,
|
|
2573
|
+
description: `Default agent for ${domain} domain`
|
|
2574
|
+
}
|
|
2575
|
+
],
|
|
2576
|
+
enforcement: {
|
|
2577
|
+
default_action: "SCMP_ACT_ERRNO",
|
|
2578
|
+
falco_enabled: false,
|
|
2579
|
+
verbose: false
|
|
2580
|
+
},
|
|
2581
|
+
xshieldai: {
|
|
2582
|
+
posture_suite: "KavachOS",
|
|
2583
|
+
homepage: "https://kavachos.xshieldai.com",
|
|
2584
|
+
license: "AGPL-3.0"
|
|
2585
|
+
},
|
|
2586
|
+
_generated_at: new Date().toISOString(),
|
|
2587
|
+
_rule_ref: "KOS-033"
|
|
2588
|
+
};
|
|
2589
|
+
writeFileSync5(configPath, JSON.stringify(config, null, 2));
|
|
2590
|
+
console.log(`
|
|
2591
|
+
KavachOS initialized: ${configPath}`);
|
|
2592
|
+
console.log(` domain: ${domain}`);
|
|
2593
|
+
console.log(` trust_mask: 0x${trustMask.toString(16).padStart(8, "0")}`);
|
|
2594
|
+
console.log(` agent_type: ${agentType}`);
|
|
2595
|
+
console.log(`
|
|
2596
|
+
Next: kavachos run <your-agent> --domain=${domain} --trust-mask=0x${trustMask.toString(16)}`);
|
|
2597
|
+
console.log(` kavachos profile show (after first run)`);
|
|
2598
|
+
}
|
|
2599
|
+
function parseRunOpts(args2) {
|
|
2600
|
+
return {
|
|
2601
|
+
trustMask: parseTrustMask(args2),
|
|
2602
|
+
domain: parseDomain(args2),
|
|
2603
|
+
agentType: args2.find((a) => a.startsWith("--agent-type="))?.split("=")[1] ?? "claude-code",
|
|
2604
|
+
sessionId: args2.find((a) => a.startsWith("--session-id="))?.split("=")[1],
|
|
2605
|
+
agentId: args2.find((a) => a.startsWith("--agent-id="))?.split("=")[1],
|
|
2606
|
+
dryRun: args2.includes("--dry-run"),
|
|
2607
|
+
verbose: args2.includes("--verbose") || args2.includes("-v"),
|
|
2608
|
+
falcoEnabled: args2.includes("--falco")
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
function parseTrustMask(args2) {
|
|
2612
|
+
const flag = args2.find((a) => a.startsWith("--trust-mask="));
|
|
2613
|
+
if (!flag) {
|
|
2614
|
+
return parseInt(process.env.KAVACHOS_TRUST_MASK ?? "1", 10);
|
|
2615
|
+
}
|
|
2616
|
+
const val = flag.split("=")[1];
|
|
2617
|
+
return val.startsWith("0x") ? parseInt(val, 16) : parseInt(val, 10);
|
|
2618
|
+
}
|
|
2619
|
+
function parseDomain(args2) {
|
|
2620
|
+
return args2.find((a) => a.startsWith("--domain="))?.split("=")[1] ?? process.env.KAVACHOS_DOMAIN ?? "general";
|
|
2621
|
+
}
|
|
2622
|
+
function outFile(base, ext) {
|
|
2623
|
+
return base.includes(".") ? `${base.replace(/\.[^.]+$/, "")}.${ext}` : `${base}.${ext}`;
|
|
2624
|
+
}
|
|
2625
|
+
function printHelp() {
|
|
2626
|
+
console.log(`
|
|
2627
|
+
kavachos \u2014 KavachOS kernel enforcement CLI
|
|
2628
|
+
Part of the xShieldAI Posture Suite \xB7 kavachos.xshieldai.com
|
|
2629
|
+
Version 2.0.0 | AGPL-3.0 | DOI 10.5281/zenodo.19908430
|
|
2630
|
+
|
|
2631
|
+
Usage: kavachos <command> [options]
|
|
2632
|
+
|
|
2633
|
+
Commands:
|
|
2634
|
+
run <binary> [args] Launch agent under seccomp-bpf governance (KOS-011)
|
|
2635
|
+
generate Generate seccomp profile + Falco rules without exec
|
|
2636
|
+
profile show [agent-id] Show active seccomp profile + gate valve state (KOS-031)
|
|
2637
|
+
audit [session-id] Verify profile hash + PRAMANA receipt chain (KOS-012)
|
|
2638
|
+
rules Print domain-specific Falco rules
|
|
2639
|
+
init Write .kavachos.json config in project root (KOS-033)
|
|
2640
|
+
version Print version
|
|
2641
|
+
|
|
2642
|
+
Options for run / generate:
|
|
2643
|
+
--trust-mask=<N> trust_mask value (hex or decimal, default: 1)
|
|
2644
|
+
--domain=<name> Domain: general|maritime|logistics|ot|finance (default: general)
|
|
2645
|
+
--agent-type=<name> Agent type label (default: claude-code)
|
|
2646
|
+
--session-id=<id> Override session ID
|
|
2647
|
+
--agent-id=<id> Agent ID for receipt chain linkage
|
|
2648
|
+
--falco Write Falco rules file alongside seccomp profile
|
|
2649
|
+
--dry-run Generate profile only, do not exec
|
|
2650
|
+
--verbose / -v Verbose kernel messages on stderr
|
|
2651
|
+
--json Output JSON (generate/rules/audit/profile)
|
|
2652
|
+
--out=<path> Write profile + rules to files (generate only)
|
|
2653
|
+
|
|
2654
|
+
Options for audit / profile show:
|
|
2655
|
+
--all List all sessions (audit) / all profiles (profile show)
|
|
2656
|
+
--session=<id> Filter profile show by session ID
|
|
2657
|
+
|
|
2658
|
+
Options for init:
|
|
2659
|
+
--force Overwrite existing .kavachos.json
|
|
2660
|
+
|
|
2661
|
+
Examples:
|
|
2662
|
+
kavachos init --domain=maritime --trust-mask=0xFF
|
|
2663
|
+
kavachos run claude --trust-mask=0xFF --domain=general --verbose
|
|
2664
|
+
kavachos run bun src/my-agent.ts --trust-mask=0x00FF0000 --domain=maritime --falco
|
|
2665
|
+
kavachos generate --trust-mask=255 --domain=logistics --json
|
|
2666
|
+
kavachos generate --trust-mask=0 --domain=general --out=/tmp/minimal
|
|
2667
|
+
kavachos profile show agent-123
|
|
2668
|
+
kavachos profile show
|
|
2669
|
+
kavachos audit KOS-A1B2C3
|
|
2670
|
+
kavachos audit --all
|
|
2671
|
+
kavachos rules --domain=maritime --trust-mask=0x0000FF00
|
|
2672
|
+
|
|
2673
|
+
Rules:
|
|
2674
|
+
KOS-011: This CLI is the only approved path for governed agent launch
|
|
2675
|
+
KOS-010: Profiles are generated deterministically \u2014 never hand-written
|
|
2676
|
+
KOS-012: Profile drift = security incident
|
|
2677
|
+
KOS-031: profile show = live posture view for any governed agent
|
|
2678
|
+
KOS-033: init = project-level config, registers agent types + domains
|
|
2679
|
+
INF-KOS-001: trust_mask=0 \u2192 read-only minimal profile
|
|
2680
|
+
INF-KOS-007: --domain absent \u2192 trust_mask=1 \u2192 minimal safe profile
|
|
2681
|
+
`);
|
|
2682
|
+
}
|
|
2683
|
+
main().catch((err) => {
|
|
2684
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2685
|
+
console.error(`kavachos error: ${message}`);
|
|
2686
|
+
process.exit(1);
|
|
2687
|
+
});
|