codebot-ai 1.5.0 → 1.7.0
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/dist/agent.d.ts +13 -0
- package/dist/agent.js +63 -4
- package/dist/audit.d.ts +61 -0
- package/dist/audit.js +231 -0
- package/dist/cli.js +181 -26
- package/dist/mcp.js +54 -3
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +71 -7
- package/dist/plugins.d.ts +0 -14
- package/dist/plugins.js +27 -14
- package/dist/policy.d.ts +123 -0
- package/dist/policy.js +418 -0
- package/dist/sandbox.d.ts +65 -0
- package/dist/sandbox.js +214 -0
- package/dist/secrets.d.ts +26 -0
- package/dist/secrets.js +86 -0
- package/dist/security.d.ts +18 -0
- package/dist/security.js +167 -0
- package/dist/telemetry.d.ts +73 -0
- package/dist/telemetry.js +286 -0
- package/dist/tools/batch-edit.js +20 -1
- package/dist/tools/edit.js +29 -5
- package/dist/tools/execute.js +81 -4
- package/dist/tools/package-manager.js +36 -0
- package/dist/tools/web-fetch.js +42 -2
- package/dist/tools/write.js +17 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.main = main;
|
|
37
37
|
const readline = __importStar(require("readline"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
38
40
|
const agent_1 = require("./agent");
|
|
39
41
|
const openai_1 = require("./providers/openai");
|
|
40
42
|
const anthropic_1 = require("./providers/anthropic");
|
|
@@ -44,9 +46,10 @@ const setup_1 = require("./setup");
|
|
|
44
46
|
const banner_1 = require("./banner");
|
|
45
47
|
const tools_1 = require("./tools");
|
|
46
48
|
const scheduler_1 = require("./scheduler");
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
const audit_1 = require("./audit");
|
|
50
|
+
const policy_1 = require("./policy");
|
|
51
|
+
const sandbox_1 = require("./sandbox");
|
|
52
|
+
const VERSION = '1.7.0';
|
|
50
53
|
const C = {
|
|
51
54
|
reset: '\x1b[0m',
|
|
52
55
|
bold: '\x1b[1m',
|
|
@@ -86,12 +89,89 @@ async function main() {
|
|
|
86
89
|
await (0, setup_1.runSetup)();
|
|
87
90
|
return;
|
|
88
91
|
}
|
|
92
|
+
// ── v1.7.0: New standalone commands ──
|
|
93
|
+
// --init-policy: Generate default policy file
|
|
94
|
+
if (args['init-policy']) {
|
|
95
|
+
const policyPath = path.join(process.cwd(), '.codebot', 'policy.json');
|
|
96
|
+
const policyDir = path.dirname(policyPath);
|
|
97
|
+
if (!fs.existsSync(policyDir))
|
|
98
|
+
fs.mkdirSync(policyDir, { recursive: true });
|
|
99
|
+
if (fs.existsSync(policyPath)) {
|
|
100
|
+
console.log(c(`Policy file already exists at ${policyPath}`, 'yellow'));
|
|
101
|
+
console.log(c('Delete it first if you want to regenerate.', 'dim'));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
fs.writeFileSync(policyPath, (0, policy_1.generateDefaultPolicyFile)(), 'utf-8');
|
|
105
|
+
console.log(c(`Created default policy at ${policyPath}`, 'green'));
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// --verify-audit: Verify audit chain integrity
|
|
110
|
+
if (args['verify-audit']) {
|
|
111
|
+
const logger = new audit_1.AuditLogger();
|
|
112
|
+
const sessionId = typeof args['verify-audit'] === 'string' ? args['verify-audit'] : undefined;
|
|
113
|
+
if (sessionId) {
|
|
114
|
+
const entries = logger.query({ sessionId });
|
|
115
|
+
if (entries.length === 0) {
|
|
116
|
+
console.log(c(`No audit entries found for session ${sessionId}`, 'yellow'));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const result = audit_1.AuditLogger.verify(entries);
|
|
120
|
+
if (result.valid) {
|
|
121
|
+
console.log(c(`Audit chain valid (${result.entriesChecked} entries checked)`, 'green'));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(c(`Audit chain INVALID at sequence ${result.firstInvalidAt}`, 'red'));
|
|
125
|
+
console.log(c(`Reason: ${result.reason}`, 'red'));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Verify all entries from today's log
|
|
130
|
+
const entries = logger.query();
|
|
131
|
+
if (entries.length === 0) {
|
|
132
|
+
console.log(c('No audit entries found.', 'yellow'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Group by session and verify each
|
|
136
|
+
const sessions = new Map();
|
|
137
|
+
for (const e of entries) {
|
|
138
|
+
if (!sessions.has(e.sessionId))
|
|
139
|
+
sessions.set(e.sessionId, []);
|
|
140
|
+
sessions.get(e.sessionId).push(e);
|
|
141
|
+
}
|
|
142
|
+
let allValid = true;
|
|
143
|
+
for (const [sid, sessionEntries] of sessions) {
|
|
144
|
+
const result = audit_1.AuditLogger.verify(sessionEntries);
|
|
145
|
+
const shortId = sid.substring(0, 12);
|
|
146
|
+
if (result.valid) {
|
|
147
|
+
console.log(c(` ${shortId} ${result.entriesChecked} entries valid`, 'green'));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.log(c(` ${shortId} INVALID at seq ${result.firstInvalidAt}: ${result.reason}`, 'red'));
|
|
151
|
+
allValid = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
console.log(allValid
|
|
155
|
+
? c(`\nAll ${sessions.size} session chains verified.`, 'green')
|
|
156
|
+
: c(`\nSome chains are invalid — possible tampering detected.`, 'red'));
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// --sandbox-info: Show sandbox status
|
|
161
|
+
if (args['sandbox-info']) {
|
|
162
|
+
const info = (0, sandbox_1.getSandboxInfo)();
|
|
163
|
+
console.log(c('Sandbox Status:', 'bold'));
|
|
164
|
+
console.log(` Docker: ${info.available ? c('available', 'green') : c('not available', 'yellow')}`);
|
|
165
|
+
console.log(` Image: ${info.image}`);
|
|
166
|
+
console.log(` CPU: ${info.defaults.cpus} cores max`);
|
|
167
|
+
console.log(` Memory: ${info.defaults.memoryMb}MB max`);
|
|
168
|
+
console.log(` Network: ${info.defaults.network ? 'enabled' : 'disabled'} by default`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
89
171
|
// First run: auto-launch setup if nothing is configured
|
|
90
172
|
if ((0, setup_1.isFirstRun)() && process.stdin.isTTY && !args.message) {
|
|
91
173
|
console.log(c('Welcome! No configuration found — launching setup...', 'cyan'));
|
|
92
174
|
await (0, setup_1.runSetup)();
|
|
93
|
-
// If setup saved a config, continue to main flow
|
|
94
|
-
// Otherwise exit
|
|
95
175
|
if ((0, setup_1.isFirstRun)())
|
|
96
176
|
return;
|
|
97
177
|
}
|
|
@@ -118,6 +198,7 @@ async function main() {
|
|
|
118
198
|
const agent = new agent_1.Agent({
|
|
119
199
|
provider,
|
|
120
200
|
model: config.model,
|
|
201
|
+
providerName: config.provider,
|
|
121
202
|
maxIterations: config.maxIterations,
|
|
122
203
|
autoApprove: config.autoApprove,
|
|
123
204
|
onMessage: (msg) => session.save(msg),
|
|
@@ -133,6 +214,7 @@ async function main() {
|
|
|
133
214
|
// Non-interactive: single message from CLI args
|
|
134
215
|
if (typeof args.message === 'string') {
|
|
135
216
|
await runOnce(agent, args.message);
|
|
217
|
+
printSessionSummary(agent);
|
|
136
218
|
return;
|
|
137
219
|
}
|
|
138
220
|
// Non-interactive: piped stdin
|
|
@@ -140,6 +222,7 @@ async function main() {
|
|
|
140
222
|
const input = await readStdin();
|
|
141
223
|
if (input.trim()) {
|
|
142
224
|
await runOnce(agent, input.trim());
|
|
225
|
+
printSessionSummary(agent);
|
|
143
226
|
}
|
|
144
227
|
return;
|
|
145
228
|
}
|
|
@@ -151,6 +234,22 @@ async function main() {
|
|
|
151
234
|
// Cleanup scheduler on exit
|
|
152
235
|
scheduler.stop();
|
|
153
236
|
}
|
|
237
|
+
/** Print session summary with tokens, cost, tool calls, files modified */
|
|
238
|
+
function printSessionSummary(agent) {
|
|
239
|
+
const tracker = agent.getTokenTracker();
|
|
240
|
+
tracker.saveUsage();
|
|
241
|
+
const summary = tracker.getSummary();
|
|
242
|
+
const duration = (new Date(summary.endTime).getTime() - new Date(summary.startTime).getTime()) / 1000;
|
|
243
|
+
const mins = Math.floor(duration / 60);
|
|
244
|
+
const secs = Math.floor(duration % 60);
|
|
245
|
+
console.log(c('\n── Session Summary ──', 'dim'));
|
|
246
|
+
console.log(` Duration: ${mins}m ${secs}s`);
|
|
247
|
+
console.log(` Model: ${summary.model} via ${summary.provider}`);
|
|
248
|
+
console.log(` Tokens: ${summary.totalInputTokens.toLocaleString()} in / ${summary.totalOutputTokens.toLocaleString()} out (${tracker.formatCost()})`);
|
|
249
|
+
console.log(` Requests: ${summary.requestCount}`);
|
|
250
|
+
console.log(` Tools: ${summary.toolCalls} calls`);
|
|
251
|
+
console.log(` Files: ${summary.filesModified} modified`);
|
|
252
|
+
}
|
|
154
253
|
function createProvider(config) {
|
|
155
254
|
if (config.provider === 'anthropic') {
|
|
156
255
|
return new anthropic_1.AnthropicProvider({
|
|
@@ -159,7 +258,6 @@ function createProvider(config) {
|
|
|
159
258
|
model: config.model,
|
|
160
259
|
});
|
|
161
260
|
}
|
|
162
|
-
// All other providers use OpenAI-compatible format
|
|
163
261
|
return new openai_1.OpenAIProvider({
|
|
164
262
|
baseUrl: config.baseUrl,
|
|
165
263
|
apiKey: config.apiKey,
|
|
@@ -186,7 +284,7 @@ async function repl(agent, config, session) {
|
|
|
186
284
|
}
|
|
187
285
|
try {
|
|
188
286
|
for await (const event of agent.run(input)) {
|
|
189
|
-
renderEvent(event);
|
|
287
|
+
renderEvent(event, agent);
|
|
190
288
|
}
|
|
191
289
|
}
|
|
192
290
|
catch (err) {
|
|
@@ -197,18 +295,19 @@ async function repl(agent, config, session) {
|
|
|
197
295
|
rl.prompt();
|
|
198
296
|
});
|
|
199
297
|
rl.on('close', () => {
|
|
298
|
+
printSessionSummary(agent);
|
|
200
299
|
console.log(c('\nBye!', 'dim'));
|
|
201
300
|
process.exit(0);
|
|
202
301
|
});
|
|
203
302
|
}
|
|
204
303
|
async function runOnce(agent, message) {
|
|
205
304
|
for await (const event of agent.run(message)) {
|
|
206
|
-
renderEvent(event);
|
|
305
|
+
renderEvent(event, agent);
|
|
207
306
|
}
|
|
208
307
|
console.log();
|
|
209
308
|
}
|
|
210
309
|
let isThinking = false;
|
|
211
|
-
function renderEvent(event) {
|
|
310
|
+
function renderEvent(event, agent) {
|
|
212
311
|
switch (event.type) {
|
|
213
312
|
case 'thinking':
|
|
214
313
|
if (!isThinking) {
|
|
@@ -248,20 +347,15 @@ function renderEvent(event) {
|
|
|
248
347
|
}
|
|
249
348
|
break;
|
|
250
349
|
case 'usage':
|
|
251
|
-
if (event.usage) {
|
|
252
|
-
|
|
253
|
-
sessionTokens.input += event.usage.inputTokens;
|
|
254
|
-
if (event.usage.outputTokens)
|
|
255
|
-
sessionTokens.output += event.usage.outputTokens;
|
|
256
|
-
if (event.usage.totalTokens)
|
|
257
|
-
sessionTokens.total += event.usage.totalTokens;
|
|
350
|
+
if (event.usage && agent) {
|
|
351
|
+
const tracker = agent.getTokenTracker();
|
|
258
352
|
const parts = [];
|
|
259
353
|
if (event.usage.inputTokens)
|
|
260
354
|
parts.push(`in: ${event.usage.inputTokens}`);
|
|
261
355
|
if (event.usage.outputTokens)
|
|
262
356
|
parts.push(`out: ${event.usage.outputTokens}`);
|
|
263
357
|
if (parts.length > 0) {
|
|
264
|
-
console.log(c(` [${parts.join(', ')} tokens]`, 'dim'));
|
|
358
|
+
console.log(c(` [${parts.join(', ')} tokens | ${tracker.formatCost()}]`, 'dim'));
|
|
265
359
|
}
|
|
266
360
|
}
|
|
267
361
|
break;
|
|
@@ -306,7 +400,10 @@ function handleSlashCommand(input, agent, config) {
|
|
|
306
400
|
/auto Toggle autonomous mode
|
|
307
401
|
/routines List scheduled routines
|
|
308
402
|
/undo Undo last file edit (/undo [path])
|
|
309
|
-
/usage Show token usage for this session
|
|
403
|
+
/usage Show token usage & cost for this session
|
|
404
|
+
/cost Show running cost
|
|
405
|
+
/policy Show current security policy
|
|
406
|
+
/audit Verify audit chain for this session
|
|
310
407
|
/config Show current config
|
|
311
408
|
/quit Exit`);
|
|
312
409
|
break;
|
|
@@ -365,10 +462,41 @@ function handleSlashCommand(input, agent, config) {
|
|
|
365
462
|
break;
|
|
366
463
|
}
|
|
367
464
|
case '/usage': {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
console.log(
|
|
371
|
-
console.log(`
|
|
465
|
+
const tracker = agent.getTokenTracker();
|
|
466
|
+
const summary = tracker.getSummary();
|
|
467
|
+
console.log(c('\nSession Usage:', 'bold'));
|
|
468
|
+
console.log(` Input: ${summary.totalInputTokens.toLocaleString()} tokens`);
|
|
469
|
+
console.log(` Output: ${summary.totalOutputTokens.toLocaleString()} tokens`);
|
|
470
|
+
console.log(` Cost: ${tracker.formatCost()}`);
|
|
471
|
+
console.log(` Requests: ${summary.requestCount}`);
|
|
472
|
+
console.log(` Tools: ${summary.toolCalls} calls`);
|
|
473
|
+
console.log(` Files: ${summary.filesModified} modified`);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case '/cost': {
|
|
477
|
+
const tracker = agent.getTokenTracker();
|
|
478
|
+
console.log(c(` ${tracker.formatStatusLine()}`, 'dim'));
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
case '/policy': {
|
|
482
|
+
const policy = agent.getPolicyEnforcer().getPolicy();
|
|
483
|
+
console.log(c('\nCurrent Policy:', 'bold'));
|
|
484
|
+
console.log(JSON.stringify(policy, null, 2));
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case '/audit': {
|
|
488
|
+
const auditLogger = agent.getAuditLogger();
|
|
489
|
+
const result = auditLogger.verifySession();
|
|
490
|
+
if (result.entriesChecked === 0) {
|
|
491
|
+
console.log(c('No audit entries yet.', 'dim'));
|
|
492
|
+
}
|
|
493
|
+
else if (result.valid) {
|
|
494
|
+
console.log(c(`Audit chain valid (${result.entriesChecked} entries)`, 'green'));
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
console.log(c(`Audit chain INVALID at sequence ${result.firstInvalidAt}`, 'red'));
|
|
498
|
+
console.log(c(` ${result.reason}`, 'red'));
|
|
499
|
+
}
|
|
372
500
|
break;
|
|
373
501
|
}
|
|
374
502
|
case '/routines': {
|
|
@@ -409,7 +537,6 @@ function showModels() {
|
|
|
409
537
|
}
|
|
410
538
|
}
|
|
411
539
|
async function resolveConfig(args) {
|
|
412
|
-
// Load saved config (CLI args override saved config)
|
|
413
540
|
const saved = (0, setup_1.loadConfig)();
|
|
414
541
|
const model = args.model || process.env.CODEBOT_MODEL || saved.model || 'qwen2.5-coder:32b';
|
|
415
542
|
const detected = (0, registry_1.detectProvider)(model);
|
|
@@ -421,7 +548,6 @@ async function resolveConfig(args) {
|
|
|
421
548
|
maxIterations: parseInt(args['max-iterations'] || String(saved.maxIterations || 50), 10),
|
|
422
549
|
autoApprove: !!args['auto-approve'] || !!args.autonomous || !!args.auto || !!saved.autoApprove,
|
|
423
550
|
};
|
|
424
|
-
// Auto-resolve base URL and API key from provider
|
|
425
551
|
if (!config.baseUrl || !config.apiKey) {
|
|
426
552
|
const defaults = registry_1.PROVIDER_DEFAULTS[config.provider];
|
|
427
553
|
if (defaults) {
|
|
@@ -431,14 +557,12 @@ async function resolveConfig(args) {
|
|
|
431
557
|
config.apiKey = process.env[defaults.envKey] || process.env.CODEBOT_API_KEY || '';
|
|
432
558
|
}
|
|
433
559
|
}
|
|
434
|
-
// Fallback: try saved config API key, then generic env vars
|
|
435
560
|
if (!config.apiKey && saved.apiKey) {
|
|
436
561
|
config.apiKey = saved.apiKey;
|
|
437
562
|
}
|
|
438
563
|
if (!config.apiKey) {
|
|
439
564
|
config.apiKey = process.env.CODEBOT_API_KEY || process.env.OPENAI_API_KEY || '';
|
|
440
565
|
}
|
|
441
|
-
// If still no base URL, auto-detect local provider
|
|
442
566
|
if (!config.baseUrl) {
|
|
443
567
|
config.baseUrl = await autoDetectProvider();
|
|
444
568
|
}
|
|
@@ -494,6 +618,25 @@ function parseArgs(argv) {
|
|
|
494
618
|
result.setup = true;
|
|
495
619
|
continue;
|
|
496
620
|
}
|
|
621
|
+
if (arg === '--init-policy') {
|
|
622
|
+
result['init-policy'] = true;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (arg === '--sandbox-info') {
|
|
626
|
+
result['sandbox-info'] = true;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (arg === '--verify-audit') {
|
|
630
|
+
const next = argv[i + 1];
|
|
631
|
+
if (next && !next.startsWith('--')) {
|
|
632
|
+
result['verify-audit'] = next;
|
|
633
|
+
i++;
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
result['verify-audit'] = true;
|
|
637
|
+
}
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
497
640
|
if (arg.startsWith('--')) {
|
|
498
641
|
const key = arg.slice(2);
|
|
499
642
|
const next = argv[i + 1];
|
|
@@ -540,9 +683,15 @@ ${c('Options:', 'bold')}
|
|
|
540
683
|
--resume <id> Resume a previous session by ID
|
|
541
684
|
--continue, -c Resume the most recent session
|
|
542
685
|
--max-iterations <n> Max agent loop iterations (default: 50)
|
|
686
|
+
--sandbox <mode> Execution sandbox: docker, host, auto (default: auto)
|
|
543
687
|
-h, --help Show this help
|
|
544
688
|
-v, --version Show version
|
|
545
689
|
|
|
690
|
+
${c('Security & Policy:', 'bold')}
|
|
691
|
+
--init-policy Generate default .codebot/policy.json
|
|
692
|
+
--verify-audit [id] Verify audit log hash chain integrity
|
|
693
|
+
--sandbox-info Show Docker sandbox status
|
|
694
|
+
|
|
546
695
|
${c('Supported Providers:', 'bold')}
|
|
547
696
|
Local: Ollama, LM Studio, vLLM (auto-detected)
|
|
548
697
|
Anthropic: Claude Opus/Sonnet/Haiku (ANTHROPIC_API_KEY)
|
|
@@ -560,6 +709,8 @@ ${c('Examples:', 'bold')}
|
|
|
560
709
|
codebot --model deepseek-chat Uses DeepSeek API
|
|
561
710
|
codebot --model qwen2.5-coder:32b Uses local Ollama
|
|
562
711
|
codebot --autonomous "refactor src/" Full auto, no prompts
|
|
712
|
+
codebot --init-policy Create security policy
|
|
713
|
+
codebot --verify-audit Check audit integrity
|
|
563
714
|
|
|
564
715
|
${c('Interactive Commands:', 'bold')}
|
|
565
716
|
/help Show commands
|
|
@@ -569,6 +720,10 @@ ${c('Interactive Commands:', 'bold')}
|
|
|
569
720
|
/auto Toggle autonomous mode
|
|
570
721
|
/clear Clear conversation
|
|
571
722
|
/compact Force context compaction
|
|
723
|
+
/usage Show token usage & cost
|
|
724
|
+
/cost Show running cost
|
|
725
|
+
/policy Show security policy
|
|
726
|
+
/audit Verify session audit chain
|
|
572
727
|
/config Show configuration
|
|
573
728
|
/quit Exit`);
|
|
574
729
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -38,6 +38,37 @@ const child_process_1 = require("child_process");
|
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const os = __importStar(require("os"));
|
|
41
|
+
/**
|
|
42
|
+
* MCP (Model Context Protocol) client.
|
|
43
|
+
*
|
|
44
|
+
* Connects to MCP servers defined in `.codebot/mcp.json` or `~/.codebot/mcp.json`:
|
|
45
|
+
*
|
|
46
|
+
* {
|
|
47
|
+
* "servers": [
|
|
48
|
+
* {
|
|
49
|
+
* "name": "my-server",
|
|
50
|
+
* "command": "npx",
|
|
51
|
+
* "args": ["-y", "@my/mcp-server"],
|
|
52
|
+
* "env": {}
|
|
53
|
+
* }
|
|
54
|
+
* ]
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* Each server is launched as a subprocess communicating via JSON-RPC over stdio.
|
|
58
|
+
*/
|
|
59
|
+
/** Allowlist of commands that MCP servers are permitted to run */
|
|
60
|
+
const ALLOWED_MCP_COMMANDS = new Set([
|
|
61
|
+
'npx', 'node', 'python', 'python3', 'deno', 'bun', 'docker', 'uvx',
|
|
62
|
+
]);
|
|
63
|
+
/** Safe environment variables to pass to MCP subprocesses */
|
|
64
|
+
const SAFE_ENV_VARS = new Set([
|
|
65
|
+
'PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'TMP', 'TEMP',
|
|
66
|
+
'LC_ALL', 'LC_CTYPE', 'DISPLAY', 'XDG_RUNTIME_DIR',
|
|
67
|
+
// Node.js
|
|
68
|
+
'NODE_ENV', 'NODE_PATH',
|
|
69
|
+
// Python
|
|
70
|
+
'PYTHONPATH', 'VIRTUAL_ENV',
|
|
71
|
+
]);
|
|
41
72
|
class MCPConnection {
|
|
42
73
|
process;
|
|
43
74
|
buffer = '';
|
|
@@ -46,9 +77,25 @@ class MCPConnection {
|
|
|
46
77
|
name;
|
|
47
78
|
constructor(config) {
|
|
48
79
|
this.name = config.name;
|
|
80
|
+
// Security: validate command against allowlist
|
|
81
|
+
const command = path.basename(config.command);
|
|
82
|
+
if (!ALLOWED_MCP_COMMANDS.has(command)) {
|
|
83
|
+
throw new Error(`Blocked MCP command: "${config.command}". Allowed: ${[...ALLOWED_MCP_COMMANDS].join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
// Security: build safe environment — only pass safe vars + config-defined vars
|
|
86
|
+
const safeEnv = {};
|
|
87
|
+
for (const key of SAFE_ENV_VARS) {
|
|
88
|
+
if (process.env[key]) {
|
|
89
|
+
safeEnv[key] = process.env[key];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Config-defined env vars override safe defaults
|
|
93
|
+
if (config.env) {
|
|
94
|
+
Object.assign(safeEnv, config.env);
|
|
95
|
+
}
|
|
49
96
|
this.process = (0, child_process_1.spawn)(config.command, config.args || [], {
|
|
50
97
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
51
|
-
env:
|
|
98
|
+
env: safeEnv,
|
|
52
99
|
});
|
|
53
100
|
this.process.stdout?.on('data', (chunk) => {
|
|
54
101
|
this.buffer += chunk.toString();
|
|
@@ -97,7 +144,7 @@ class MCPConnection {
|
|
|
97
144
|
await this.request('initialize', {
|
|
98
145
|
protocolVersion: '2024-11-05',
|
|
99
146
|
capabilities: {},
|
|
100
|
-
clientInfo: { name: 'codebot-ai', version: '1.
|
|
147
|
+
clientInfo: { name: 'codebot-ai', version: '1.6.0' },
|
|
101
148
|
});
|
|
102
149
|
await this.request('notifications/initialized');
|
|
103
150
|
}
|
|
@@ -121,9 +168,13 @@ class MCPConnection {
|
|
|
121
168
|
}
|
|
122
169
|
/** Create Tool wrappers from an MCP server's tools */
|
|
123
170
|
function mcpToolToTool(connection, def) {
|
|
171
|
+
// Security: sanitize tool description (limit length, strip control chars)
|
|
172
|
+
const safeDescription = (def.description || '')
|
|
173
|
+
.substring(0, 500)
|
|
174
|
+
.replace(/[\x00-\x1F\x7F]/g, '');
|
|
124
175
|
return {
|
|
125
176
|
name: `mcp_${connection.name}_${def.name}`,
|
|
126
|
-
description: `[MCP:${connection.name}] ${
|
|
177
|
+
description: `[MCP:${connection.name}] ${safeDescription}`,
|
|
127
178
|
permission: 'prompt',
|
|
128
179
|
parameters: def.inputSchema || { type: 'object', properties: {} },
|
|
129
180
|
execute: async (args) => {
|
package/dist/memory.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize memory content by stripping lines that look like prompt injection.
|
|
3
|
+
*/
|
|
4
|
+
export declare function sanitizeMemory(content: string): string;
|
|
1
5
|
export interface MemoryEntry {
|
|
2
6
|
key: string;
|
|
3
7
|
value: string;
|
|
@@ -8,6 +12,9 @@ export interface MemoryEntry {
|
|
|
8
12
|
* Persistent memory system for CodeBot.
|
|
9
13
|
* Stores project-level and global notes that survive across sessions.
|
|
10
14
|
* Memory is injected into the system prompt so the model always has context.
|
|
15
|
+
*
|
|
16
|
+
* Security: content is sanitized before injection to prevent prompt injection.
|
|
17
|
+
* Size limits: 2KB per file, 10KB total.
|
|
11
18
|
*/
|
|
12
19
|
export declare class MemoryManager {
|
|
13
20
|
private projectDir;
|
package/dist/memory.js
CHANGED
|
@@ -34,15 +34,57 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MemoryManager = void 0;
|
|
37
|
+
exports.sanitizeMemory = sanitizeMemory;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const os = __importStar(require("os"));
|
|
40
41
|
const MEMORY_DIR = path.join(os.homedir(), '.codebot', 'memory');
|
|
41
42
|
const GLOBAL_MEMORY = path.join(MEMORY_DIR, 'MEMORY.md');
|
|
43
|
+
/** Maximum size per memory file (2KB) */
|
|
44
|
+
const MAX_FILE_SIZE = 2048;
|
|
45
|
+
/** Maximum total memory size across all files (10KB) */
|
|
46
|
+
const MAX_TOTAL_SIZE = 10240;
|
|
47
|
+
/** Patterns that indicate potential prompt injection in memory content */
|
|
48
|
+
const INJECTION_PATTERNS = [
|
|
49
|
+
/^(system|assistant|user):\s/i,
|
|
50
|
+
/ignore (previous|all|above) instructions/i,
|
|
51
|
+
/you are now/i,
|
|
52
|
+
/new instructions:/i,
|
|
53
|
+
/override:/i,
|
|
54
|
+
/<\/?system>/i,
|
|
55
|
+
/\bforget (all|everything|your)\b/i,
|
|
56
|
+
/\bact as\b/i,
|
|
57
|
+
/\brole:\s*(system|admin)/i,
|
|
58
|
+
/\bpretend (you are|to be)\b/i,
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Sanitize memory content by stripping lines that look like prompt injection.
|
|
62
|
+
*/
|
|
63
|
+
function sanitizeMemory(content) {
|
|
64
|
+
return content.split('\n')
|
|
65
|
+
.filter(line => !INJECTION_PATTERNS.some(p => p.test(line)))
|
|
66
|
+
.join('\n');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Truncate content to a maximum byte size.
|
|
70
|
+
*/
|
|
71
|
+
function truncateToSize(content, maxSize) {
|
|
72
|
+
if (Buffer.byteLength(content, 'utf-8') <= maxSize)
|
|
73
|
+
return content;
|
|
74
|
+
// Truncate by chars (approximation — will be close to byte limit)
|
|
75
|
+
let truncated = content;
|
|
76
|
+
while (Buffer.byteLength(truncated, 'utf-8') > maxSize - 50) { // leave room for marker
|
|
77
|
+
truncated = truncated.substring(0, Math.floor(truncated.length * 0.9));
|
|
78
|
+
}
|
|
79
|
+
return truncated.trimEnd() + '\n[truncated — exceeded size limit]';
|
|
80
|
+
}
|
|
42
81
|
/**
|
|
43
82
|
* Persistent memory system for CodeBot.
|
|
44
83
|
* Stores project-level and global notes that survive across sessions.
|
|
45
84
|
* Memory is injected into the system prompt so the model always has context.
|
|
85
|
+
*
|
|
86
|
+
* Security: content is sanitized before injection to prevent prompt injection.
|
|
87
|
+
* Size limits: 2KB per file, 10KB total.
|
|
46
88
|
*/
|
|
47
89
|
class MemoryManager {
|
|
48
90
|
projectDir;
|
|
@@ -76,14 +118,16 @@ class MemoryManager {
|
|
|
76
118
|
}
|
|
77
119
|
/** Write to global memory */
|
|
78
120
|
writeGlobal(content) {
|
|
79
|
-
|
|
121
|
+
const safe = truncateToSize(content, MAX_FILE_SIZE);
|
|
122
|
+
fs.writeFileSync(GLOBAL_MEMORY, safe);
|
|
80
123
|
}
|
|
81
124
|
/** Write to project memory */
|
|
82
125
|
writeProject(content) {
|
|
83
126
|
if (!this.projectDir)
|
|
84
127
|
return;
|
|
85
128
|
const memFile = path.join(this.projectDir, 'MEMORY.md');
|
|
86
|
-
|
|
129
|
+
const safe = truncateToSize(content, MAX_FILE_SIZE);
|
|
130
|
+
fs.writeFileSync(memFile, safe);
|
|
87
131
|
}
|
|
88
132
|
/** Append an entry to global memory */
|
|
89
133
|
appendGlobal(entry) {
|
|
@@ -114,20 +158,34 @@ class MemoryManager {
|
|
|
114
158
|
/** Get all memory content formatted for system prompt injection */
|
|
115
159
|
getContextBlock() {
|
|
116
160
|
const parts = [];
|
|
161
|
+
let totalSize = 0;
|
|
117
162
|
const global = this.readGlobal();
|
|
118
163
|
if (global.trim()) {
|
|
119
|
-
|
|
164
|
+
const sanitized = sanitizeMemory(global.trim());
|
|
165
|
+
const truncated = truncateToSize(sanitized, MAX_FILE_SIZE);
|
|
166
|
+
totalSize += Buffer.byteLength(truncated, 'utf-8');
|
|
167
|
+
parts.push(`## Global Memory\n${truncated}`);
|
|
120
168
|
}
|
|
121
169
|
// Read additional global topic files
|
|
122
170
|
const globalFiles = this.readDir(this.globalDir);
|
|
123
171
|
for (const [name, content] of Object.entries(globalFiles)) {
|
|
124
172
|
if (name === 'MEMORY.md' || !content.trim())
|
|
125
173
|
continue;
|
|
126
|
-
|
|
174
|
+
if (totalSize >= MAX_TOTAL_SIZE)
|
|
175
|
+
break;
|
|
176
|
+
const sanitized = sanitizeMemory(content.trim());
|
|
177
|
+
const remaining = MAX_TOTAL_SIZE - totalSize;
|
|
178
|
+
const truncated = truncateToSize(sanitized, Math.min(MAX_FILE_SIZE, remaining));
|
|
179
|
+
totalSize += Buffer.byteLength(truncated, 'utf-8');
|
|
180
|
+
parts.push(`## ${name.replace('.md', '')}\n${truncated}`);
|
|
127
181
|
}
|
|
128
182
|
const project = this.readProject();
|
|
129
|
-
if (project.trim()) {
|
|
130
|
-
|
|
183
|
+
if (project.trim() && totalSize < MAX_TOTAL_SIZE) {
|
|
184
|
+
const sanitized = sanitizeMemory(project.trim());
|
|
185
|
+
const remaining = MAX_TOTAL_SIZE - totalSize;
|
|
186
|
+
const truncated = truncateToSize(sanitized, Math.min(MAX_FILE_SIZE, remaining));
|
|
187
|
+
totalSize += Buffer.byteLength(truncated, 'utf-8');
|
|
188
|
+
parts.push(`## Project Memory\n${truncated}`);
|
|
131
189
|
}
|
|
132
190
|
// Read additional project topic files
|
|
133
191
|
if (this.projectDir) {
|
|
@@ -135,7 +193,13 @@ class MemoryManager {
|
|
|
135
193
|
for (const [name, content] of Object.entries(projFiles)) {
|
|
136
194
|
if (name === 'MEMORY.md' || !content.trim())
|
|
137
195
|
continue;
|
|
138
|
-
|
|
196
|
+
if (totalSize >= MAX_TOTAL_SIZE)
|
|
197
|
+
break;
|
|
198
|
+
const sanitized = sanitizeMemory(content.trim());
|
|
199
|
+
const remaining = MAX_TOTAL_SIZE - totalSize;
|
|
200
|
+
const truncated = truncateToSize(sanitized, Math.min(MAX_FILE_SIZE, remaining));
|
|
201
|
+
totalSize += Buffer.byteLength(truncated, 'utf-8');
|
|
202
|
+
parts.push(`## Project: ${name.replace('.md', '')}\n${truncated}`);
|
|
139
203
|
}
|
|
140
204
|
}
|
|
141
205
|
if (parts.length === 0)
|
package/dist/plugins.d.ts
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
1
|
import { Tool } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* Plugin system for CodeBot.
|
|
4
|
-
*
|
|
5
|
-
* Plugins are .js files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global).
|
|
6
|
-
* Each plugin exports a default function or object that implements the Tool interface:
|
|
7
|
-
*
|
|
8
|
-
* module.exports = {
|
|
9
|
-
* name: 'my_tool',
|
|
10
|
-
* description: 'Does something useful',
|
|
11
|
-
* permission: 'prompt',
|
|
12
|
-
* parameters: { type: 'object', properties: { ... }, required: [...] },
|
|
13
|
-
* execute: async (args) => { return 'result'; }
|
|
14
|
-
* };
|
|
15
|
-
*/
|
|
16
2
|
export declare function loadPlugins(projectRoot?: string): Tool[];
|
|
17
3
|
//# sourceMappingURL=plugins.d.ts.map
|
package/dist/plugins.js
CHANGED
|
@@ -36,20 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.loadPlugins = loadPlugins;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
-
|
|
40
|
-
* Plugin system for CodeBot.
|
|
41
|
-
*
|
|
42
|
-
* Plugins are .js files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global).
|
|
43
|
-
* Each plugin exports a default function or object that implements the Tool interface:
|
|
44
|
-
*
|
|
45
|
-
* module.exports = {
|
|
46
|
-
* name: 'my_tool',
|
|
47
|
-
* description: 'Does something useful',
|
|
48
|
-
* permission: 'prompt',
|
|
49
|
-
* parameters: { type: 'object', properties: { ... }, required: [...] },
|
|
50
|
-
* execute: async (args) => { return 'result'; }
|
|
51
|
-
* };
|
|
52
|
-
*/
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
53
40
|
function loadPlugins(projectRoot) {
|
|
54
41
|
const plugins = [];
|
|
55
42
|
const os = require('os');
|
|
@@ -74,6 +61,32 @@ function loadPlugins(projectRoot) {
|
|
|
74
61
|
continue;
|
|
75
62
|
try {
|
|
76
63
|
const pluginPath = path.join(dir, entry.name);
|
|
64
|
+
// Security: verify plugin against manifest hash
|
|
65
|
+
const manifestPath = path.join(dir, 'plugin.json');
|
|
66
|
+
if (!fs.existsSync(manifestPath)) {
|
|
67
|
+
console.error(`Plugin skipped (${entry.name}): no plugin.json manifest found. Create one with: { "name": "...", "version": "...", "hash": "sha256:..." }`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let manifest;
|
|
71
|
+
try {
|
|
72
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
console.error(`Plugin skipped (${entry.name}): invalid plugin.json manifest`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!manifest.hash || !manifest.hash.startsWith('sha256:')) {
|
|
79
|
+
console.error(`Plugin skipped (${entry.name}): manifest missing valid sha256 hash`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Compute SHA-256 of the plugin file
|
|
83
|
+
const pluginContent = fs.readFileSync(pluginPath);
|
|
84
|
+
const computedHash = 'sha256:' + crypto.createHash('sha256').update(pluginContent).digest('hex');
|
|
85
|
+
if (computedHash !== manifest.hash) {
|
|
86
|
+
console.error(`Plugin skipped (${entry.name}): hash mismatch. Expected ${manifest.hash}, got ${computedHash}. Plugin may have been tampered with.`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Hash verified — safe to load
|
|
77
90
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
78
91
|
const mod = require(pluginPath);
|
|
79
92
|
const plugin = mod.default || mod;
|