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 CHANGED
@@ -1,4 +1,7 @@
1
1
  import { Message, AgentEvent, LLMProvider } from './types';
2
+ import { AuditLogger } from './audit';
3
+ import { PolicyEnforcer } from './policy';
4
+ import { TokenTracker } from './telemetry';
2
5
  export declare class Agent {
3
6
  private provider;
4
7
  private tools;
@@ -9,11 +12,15 @@ export declare class Agent {
9
12
  private model;
10
13
  private cache;
11
14
  private rateLimiter;
15
+ private auditLogger;
16
+ private policyEnforcer;
17
+ private tokenTracker;
12
18
  private askPermission;
13
19
  private onMessage?;
14
20
  constructor(opts: {
15
21
  provider: LLMProvider;
16
22
  model: string;
23
+ providerName?: string;
17
24
  maxIterations?: number;
18
25
  autoApprove?: boolean;
19
26
  askPermission?: (tool: string, args: Record<string, unknown>) => Promise<boolean>;
@@ -30,6 +37,12 @@ export declare class Agent {
30
37
  after: number;
31
38
  };
32
39
  getMessages(): Message[];
40
+ /** Get the token tracker for session summary / CLI display */
41
+ getTokenTracker(): TokenTracker;
42
+ /** Get the policy enforcer for inspection */
43
+ getPolicyEnforcer(): PolicyEnforcer;
44
+ /** Get the audit logger for verification */
45
+ getAuditLogger(): AuditLogger;
33
46
  /**
34
47
  * Validate and repair message history to prevent OpenAI 400 errors.
35
48
  * Handles three types of corruption:
package/dist/agent.js CHANGED
@@ -45,6 +45,9 @@ const registry_1 = require("./providers/registry");
45
45
  const plugins_1 = require("./plugins");
46
46
  const cache_1 = require("./cache");
47
47
  const rate_limiter_1 = require("./rate-limiter");
48
+ const audit_1 = require("./audit");
49
+ const policy_1 = require("./policy");
50
+ const telemetry_1 = require("./telemetry");
48
51
  /** Lightweight schema validation — returns error string or null if valid */
49
52
  function validateToolArgs(args, schema) {
50
53
  const props = schema.properties;
@@ -98,6 +101,9 @@ class Agent {
98
101
  model;
99
102
  cache;
100
103
  rateLimiter;
104
+ auditLogger;
105
+ policyEnforcer;
106
+ tokenTracker;
101
107
  askPermission;
102
108
  onMessage;
103
109
  constructor(opts) {
@@ -105,12 +111,21 @@ class Agent {
105
111
  this.model = opts.model;
106
112
  this.tools = new tools_1.ToolRegistry(process.cwd());
107
113
  this.context = new manager_1.ContextManager(opts.model, opts.provider);
108
- this.maxIterations = opts.maxIterations || 25;
114
+ // Load policy
115
+ this.policyEnforcer = new policy_1.PolicyEnforcer((0, policy_1.loadPolicy)(process.cwd()), process.cwd());
116
+ // Use policy-defined max iterations as default, CLI overrides
117
+ this.maxIterations = opts.maxIterations || this.policyEnforcer.getMaxIterations();
109
118
  this.autoApprove = opts.autoApprove || false;
110
119
  this.askPermission = opts.askPermission || defaultAskPermission;
111
120
  this.onMessage = opts.onMessage;
112
121
  this.cache = new cache_1.ToolCache();
113
122
  this.rateLimiter = new rate_limiter_1.RateLimiter();
123
+ this.auditLogger = new audit_1.AuditLogger();
124
+ // Token & cost tracking
125
+ this.tokenTracker = new telemetry_1.TokenTracker(opts.model, opts.providerName || 'unknown');
126
+ const costLimit = this.policyEnforcer.getCostLimitUsd();
127
+ if (costLimit > 0)
128
+ this.tokenTracker.setCostLimit(costLimit);
114
129
  // Load plugins
115
130
  try {
116
131
  const plugins = (0, plugins_1.loadPlugins)(process.cwd());
@@ -177,6 +192,10 @@ class Agent {
177
192
  }
178
193
  break;
179
194
  case 'usage':
195
+ // Track tokens and cost
196
+ if (event.usage) {
197
+ this.tokenTracker.recordUsage(event.usage.inputTokens || 0, event.usage.outputTokens || 0);
198
+ }
180
199
  yield { type: 'usage', usage: event.usage };
181
200
  break;
182
201
  case 'error':
@@ -228,6 +247,11 @@ class Agent {
228
247
  yield { type: 'done' };
229
248
  return;
230
249
  }
250
+ // Cost budget check: stop if over limit
251
+ if (this.tokenTracker.isOverBudget()) {
252
+ yield { type: 'error', error: `Cost limit exceeded ($${this.tokenTracker.getTotalCost().toFixed(4)} / $${this.policyEnforcer.getCostLimitUsd().toFixed(2)}). Stopping.` };
253
+ return;
254
+ }
231
255
  const prepared = [];
232
256
  for (const tc of toolCalls) {
233
257
  const toolName = tc.function.name;
@@ -236,6 +260,13 @@ class Agent {
236
260
  prepared.push({ tc, tool: null, args: {}, denied: false, error: `Error: Unknown tool "${toolName}"` });
237
261
  continue;
238
262
  }
263
+ // Policy check: is this tool allowed?
264
+ const policyCheck = this.policyEnforcer.isToolAllowed(toolName);
265
+ if (!policyCheck.allowed) {
266
+ this.auditLogger.log({ tool: toolName, action: 'policy_block', args: {}, reason: policyCheck.reason });
267
+ prepared.push({ tc, tool, args: {}, denied: false, error: `Error: ${policyCheck.reason}` });
268
+ continue;
269
+ }
239
270
  let args;
240
271
  try {
241
272
  args = JSON.parse(tc.function.arguments);
@@ -251,14 +282,17 @@ class Agent {
251
282
  continue;
252
283
  }
253
284
  yield { type: 'tool_call', toolCall: { name: toolName, args } };
254
- // Permission check (sequential needs user interaction)
255
- const needsPermission = tool.permission === 'always-ask' ||
256
- (tool.permission === 'prompt' && !this.autoApprove);
285
+ // Permission check: policy override > tool default
286
+ const policyPermission = this.policyEnforcer.getToolPermission(toolName);
287
+ const effectivePermission = policyPermission || tool.permission;
288
+ const needsPermission = effectivePermission === 'always-ask' ||
289
+ (effectivePermission === 'prompt' && !this.autoApprove);
257
290
  let denied = false;
258
291
  if (needsPermission) {
259
292
  const approved = await this.askPermission(toolName, args);
260
293
  if (!approved) {
261
294
  denied = true;
295
+ this.auditLogger.log({ tool: toolName, action: 'deny', args, reason: 'User denied permission' });
262
296
  }
263
297
  }
264
298
  prepared.push({ tc, tool, args, denied });
@@ -304,6 +338,13 @@ class Agent {
304
338
  await this.rateLimiter.throttle(toolName);
305
339
  try {
306
340
  const output = await prep.tool.execute(prep.args);
341
+ // Audit log: successful execution
342
+ this.auditLogger.log({ tool: toolName, action: 'execute', args: prep.args, result: 'success' });
343
+ // Telemetry: track tool calls and file modifications
344
+ this.tokenTracker.recordToolCall();
345
+ if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && prep.args.path) {
346
+ this.tokenTracker.recordFileModified(prep.args.path);
347
+ }
307
348
  // Store in cache for cacheable tools
308
349
  if (prep.tool.cacheable) {
309
350
  const ttl = cache_1.ToolCache.TTL[toolName] || 30_000;
@@ -315,10 +356,16 @@ class Agent {
315
356
  if (filePath)
316
357
  this.cache.invalidate(filePath);
317
358
  }
359
+ // Audit log: check if tool returned a security block
360
+ if (output.startsWith('Error: Blocked:') || output.startsWith('Error: CWD')) {
361
+ this.auditLogger.log({ tool: toolName, action: 'security_block', args: prep.args, reason: output });
362
+ }
318
363
  return { content: output };
319
364
  }
320
365
  catch (err) {
321
366
  const errMsg = err instanceof Error ? err.message : String(err);
367
+ // Audit log: error
368
+ this.auditLogger.log({ tool: toolName, action: 'error', args: prep.args, result: 'error', reason: errMsg });
322
369
  return { content: `Error: ${errMsg}`, is_error: true };
323
370
  }
324
371
  };
@@ -375,6 +422,18 @@ class Agent {
375
422
  getMessages() {
376
423
  return [...this.messages];
377
424
  }
425
+ /** Get the token tracker for session summary / CLI display */
426
+ getTokenTracker() {
427
+ return this.tokenTracker;
428
+ }
429
+ /** Get the policy enforcer for inspection */
430
+ getPolicyEnforcer() {
431
+ return this.policyEnforcer;
432
+ }
433
+ /** Get the audit logger for verification */
434
+ getAuditLogger() {
435
+ return this.auditLogger;
436
+ }
378
437
  /**
379
438
  * Validate and repair message history to prevent OpenAI 400 errors.
380
439
  * Handles three types of corruption:
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Audit logger for CodeBot v1.7.0
3
+ *
4
+ * Provides append-only JSONL logging of all security-relevant actions.
5
+ * Logs are stored at ~/.codebot/audit/audit-YYYY-MM-DD.jsonl
6
+ *
7
+ * v1.7.0: Hash-chained entries for tamper detection.
8
+ * Each entry includes a SHA-256 hash of (prevHash + entry content).
9
+ * Verification walks the chain and detects any modifications.
10
+ *
11
+ * NEVER throws — audit failures must not crash the agent.
12
+ */
13
+ export interface AuditEntry {
14
+ timestamp: string;
15
+ sessionId: string;
16
+ sequence: number;
17
+ tool: string;
18
+ action: 'execute' | 'deny' | 'error' | 'security_block' | 'policy_block';
19
+ args: Record<string, unknown>;
20
+ result?: string;
21
+ reason?: string;
22
+ prevHash: string;
23
+ hash: string;
24
+ }
25
+ /** Result of verifying an audit chain */
26
+ export interface VerifyResult {
27
+ valid: boolean;
28
+ entriesChecked: number;
29
+ firstInvalidAt?: number;
30
+ reason?: string;
31
+ }
32
+ export declare class AuditLogger {
33
+ private logDir;
34
+ private sessionId;
35
+ private sequence;
36
+ private prevHash;
37
+ constructor(logDir?: string);
38
+ getSessionId(): string;
39
+ /** Append a hash-chained audit entry to the log file */
40
+ log(entry: Omit<AuditEntry, 'timestamp' | 'sessionId' | 'sequence' | 'prevHash' | 'hash'>): void;
41
+ /** Read log entries, optionally filtered */
42
+ query(filter?: {
43
+ tool?: string;
44
+ action?: string;
45
+ since?: string;
46
+ sessionId?: string;
47
+ }): AuditEntry[];
48
+ /**
49
+ * Verify the hash chain integrity of audit entries.
50
+ * Walks through entries for a given session and checks each hash.
51
+ */
52
+ static verify(entries: AuditEntry[]): VerifyResult;
53
+ /**
54
+ * Verify all entries for a given session.
55
+ */
56
+ verifySession(sessionId?: string): VerifyResult;
57
+ private getLogFilePath;
58
+ private rotateIfNeeded;
59
+ private sanitizeArgs;
60
+ }
61
+ //# sourceMappingURL=audit.d.ts.map
package/dist/audit.js ADDED
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AuditLogger = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const crypto = __importStar(require("crypto"));
41
+ const secrets_1 = require("./secrets");
42
+ const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB before rotation
43
+ const MAX_ARG_LENGTH = 500;
44
+ const GENESIS_HASH = 'genesis';
45
+ class AuditLogger {
46
+ logDir;
47
+ sessionId;
48
+ sequence = 0;
49
+ prevHash = GENESIS_HASH;
50
+ constructor(logDir) {
51
+ this.logDir = logDir || path.join(os.homedir(), '.codebot', 'audit');
52
+ this.sessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
53
+ try {
54
+ fs.mkdirSync(this.logDir, { recursive: true });
55
+ }
56
+ catch {
57
+ // Can't create dir — logging will be disabled
58
+ }
59
+ }
60
+ getSessionId() {
61
+ return this.sessionId;
62
+ }
63
+ /** Append a hash-chained audit entry to the log file */
64
+ log(entry) {
65
+ try {
66
+ this.sequence++;
67
+ // Build entry without hash first (hash is computed over the other fields)
68
+ const partial = {
69
+ timestamp: new Date().toISOString(),
70
+ sessionId: this.sessionId,
71
+ sequence: this.sequence,
72
+ tool: entry.tool,
73
+ action: entry.action,
74
+ args: this.sanitizeArgs(entry.args),
75
+ result: entry.result,
76
+ reason: entry.reason,
77
+ prevHash: this.prevHash,
78
+ };
79
+ // Compute hash: SHA-256 of (prevHash + JSON of partial entry)
80
+ const hashInput = this.prevHash + JSON.stringify(partial);
81
+ const hash = crypto.createHash('sha256').update(hashInput).digest('hex');
82
+ const fullEntry = { ...partial, hash };
83
+ this.prevHash = hash;
84
+ const logFile = this.getLogFilePath();
85
+ const line = JSON.stringify(fullEntry) + '\n';
86
+ this.rotateIfNeeded(logFile);
87
+ fs.appendFileSync(logFile, line, 'utf-8');
88
+ }
89
+ catch {
90
+ // Audit failures must NEVER crash the agent
91
+ }
92
+ }
93
+ /** Read log entries, optionally filtered */
94
+ query(filter) {
95
+ const entries = [];
96
+ try {
97
+ const files = fs.readdirSync(this.logDir)
98
+ .filter(f => f.startsWith('audit-') && f.endsWith('.jsonl'))
99
+ .sort();
100
+ for (const file of files) {
101
+ const content = fs.readFileSync(path.join(this.logDir, file), 'utf-8');
102
+ for (const line of content.split('\n')) {
103
+ if (!line.trim())
104
+ continue;
105
+ try {
106
+ const entry = JSON.parse(line);
107
+ if (filter?.tool && entry.tool !== filter.tool)
108
+ continue;
109
+ if (filter?.action && entry.action !== filter.action)
110
+ continue;
111
+ if (filter?.since && entry.timestamp < filter.since)
112
+ continue;
113
+ if (filter?.sessionId && entry.sessionId !== filter.sessionId)
114
+ continue;
115
+ entries.push(entry);
116
+ }
117
+ catch { /* skip malformed */ }
118
+ }
119
+ }
120
+ }
121
+ catch {
122
+ // Can't read logs
123
+ }
124
+ return entries;
125
+ }
126
+ /**
127
+ * Verify the hash chain integrity of audit entries.
128
+ * Walks through entries for a given session and checks each hash.
129
+ */
130
+ static verify(entries) {
131
+ if (entries.length === 0) {
132
+ return { valid: true, entriesChecked: 0 };
133
+ }
134
+ // Sort by sequence
135
+ const sorted = [...entries].sort((a, b) => a.sequence - b.sequence);
136
+ for (let i = 0; i < sorted.length; i++) {
137
+ const entry = sorted[i];
138
+ // Check sequence continuity
139
+ if (i === 0 && entry.prevHash !== GENESIS_HASH) {
140
+ // First entry should reference genesis (unless it's a continuation)
141
+ // Allow non-genesis for continuation of previous sessions
142
+ }
143
+ // Recompute hash
144
+ const partial = {
145
+ timestamp: entry.timestamp,
146
+ sessionId: entry.sessionId,
147
+ sequence: entry.sequence,
148
+ tool: entry.tool,
149
+ action: entry.action,
150
+ args: entry.args,
151
+ result: entry.result,
152
+ reason: entry.reason,
153
+ prevHash: entry.prevHash,
154
+ };
155
+ const hashInput = entry.prevHash + JSON.stringify(partial);
156
+ const expectedHash = crypto.createHash('sha256').update(hashInput).digest('hex');
157
+ if (entry.hash !== expectedHash) {
158
+ return {
159
+ valid: false,
160
+ entriesChecked: i + 1,
161
+ firstInvalidAt: entry.sequence,
162
+ reason: `Hash mismatch at sequence ${entry.sequence}: expected ${expectedHash.substring(0, 16)}..., got ${entry.hash.substring(0, 16)}...`,
163
+ };
164
+ }
165
+ // Check chain continuity (sequence i+1 should reference sequence i's hash)
166
+ if (i < sorted.length - 1) {
167
+ const next = sorted[i + 1];
168
+ if (next.prevHash !== entry.hash) {
169
+ return {
170
+ valid: false,
171
+ entriesChecked: i + 2,
172
+ firstInvalidAt: next.sequence,
173
+ reason: `Chain break at sequence ${next.sequence}: prevHash doesn't match previous entry's hash`,
174
+ };
175
+ }
176
+ }
177
+ }
178
+ return { valid: true, entriesChecked: sorted.length };
179
+ }
180
+ /**
181
+ * Verify all entries for a given session.
182
+ */
183
+ verifySession(sessionId) {
184
+ const sid = sessionId || this.sessionId;
185
+ const entries = this.query({ sessionId: sid });
186
+ return AuditLogger.verify(entries);
187
+ }
188
+ getLogFilePath() {
189
+ const date = new Date().toISOString().split('T')[0];
190
+ return path.join(this.logDir, `audit-${date}.jsonl`);
191
+ }
192
+ rotateIfNeeded(logFile) {
193
+ try {
194
+ if (!fs.existsSync(logFile))
195
+ return;
196
+ const stat = fs.statSync(logFile);
197
+ if (stat.size >= MAX_LOG_SIZE) {
198
+ const rotated = logFile.replace('.jsonl', `-${Date.now()}.jsonl`);
199
+ fs.renameSync(logFile, rotated);
200
+ }
201
+ }
202
+ catch {
203
+ // Rotation failure is non-fatal
204
+ }
205
+ }
206
+ sanitizeArgs(args) {
207
+ const sanitized = {};
208
+ for (const [key, value] of Object.entries(args)) {
209
+ if (typeof value === 'string') {
210
+ let masked = (0, secrets_1.maskSecretsInString)(value);
211
+ if (masked.length > MAX_ARG_LENGTH) {
212
+ masked = masked.substring(0, MAX_ARG_LENGTH) + `... (${value.length} chars)`;
213
+ }
214
+ sanitized[key] = masked;
215
+ }
216
+ else if (typeof value === 'object' && value !== null) {
217
+ const str = JSON.stringify(value);
218
+ const masked = (0, secrets_1.maskSecretsInString)(str);
219
+ sanitized[key] = masked.length > MAX_ARG_LENGTH
220
+ ? masked.substring(0, MAX_ARG_LENGTH) + '...'
221
+ : masked;
222
+ }
223
+ else {
224
+ sanitized[key] = value;
225
+ }
226
+ }
227
+ return sanitized;
228
+ }
229
+ }
230
+ exports.AuditLogger = AuditLogger;
231
+ //# sourceMappingURL=audit.js.map