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/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 VERSION = '1.5.0';
48
- // Session-wide token tracking
49
- let sessionTokens = { input: 0, output: 0, total: 0 };
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
- if (event.usage.inputTokens)
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
- console.log(c('\nToken Usage (this session):', 'bold'));
369
- console.log(` Input: ${sessionTokens.input.toLocaleString()} tokens`);
370
- console.log(` Output: ${sessionTokens.output.toLocaleString()} tokens`);
371
- console.log(` Total: ${(sessionTokens.input + sessionTokens.output).toLocaleString()} tokens`);
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: { ...process.env, ...config.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.1.0' },
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}] ${def.description}`,
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
- fs.writeFileSync(GLOBAL_MEMORY, content);
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
- fs.writeFileSync(memFile, content);
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
- parts.push(`## Global Memory\n${global.trim()}`);
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
- parts.push(`## ${name.replace('.md', '')}\n${content.trim()}`);
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
- parts.push(`## Project Memory\n${project.trim()}`);
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
- parts.push(`## Project: ${name.replace('.md', '')}\n${content.trim()}`);
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;