codebot-ai 1.6.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;
@@ -10,11 +13,14 @@ export declare class Agent {
10
13
  private cache;
11
14
  private rateLimiter;
12
15
  private auditLogger;
16
+ private policyEnforcer;
17
+ private tokenTracker;
13
18
  private askPermission;
14
19
  private onMessage?;
15
20
  constructor(opts: {
16
21
  provider: LLMProvider;
17
22
  model: string;
23
+ providerName?: string;
18
24
  maxIterations?: number;
19
25
  autoApprove?: boolean;
20
26
  askPermission?: (tool: string, args: Record<string, unknown>) => Promise<boolean>;
@@ -31,6 +37,12 @@ export declare class Agent {
31
37
  after: number;
32
38
  };
33
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;
34
46
  /**
35
47
  * Validate and repair message history to prevent OpenAI 400 errors.
36
48
  * Handles three types of corruption:
package/dist/agent.js CHANGED
@@ -46,6 +46,8 @@ const plugins_1 = require("./plugins");
46
46
  const cache_1 = require("./cache");
47
47
  const rate_limiter_1 = require("./rate-limiter");
48
48
  const audit_1 = require("./audit");
49
+ const policy_1 = require("./policy");
50
+ const telemetry_1 = require("./telemetry");
49
51
  /** Lightweight schema validation — returns error string or null if valid */
50
52
  function validateToolArgs(args, schema) {
51
53
  const props = schema.properties;
@@ -100,6 +102,8 @@ class Agent {
100
102
  cache;
101
103
  rateLimiter;
102
104
  auditLogger;
105
+ policyEnforcer;
106
+ tokenTracker;
103
107
  askPermission;
104
108
  onMessage;
105
109
  constructor(opts) {
@@ -107,13 +111,21 @@ class Agent {
107
111
  this.model = opts.model;
108
112
  this.tools = new tools_1.ToolRegistry(process.cwd());
109
113
  this.context = new manager_1.ContextManager(opts.model, opts.provider);
110
- 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();
111
118
  this.autoApprove = opts.autoApprove || false;
112
119
  this.askPermission = opts.askPermission || defaultAskPermission;
113
120
  this.onMessage = opts.onMessage;
114
121
  this.cache = new cache_1.ToolCache();
115
122
  this.rateLimiter = new rate_limiter_1.RateLimiter();
116
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);
117
129
  // Load plugins
118
130
  try {
119
131
  const plugins = (0, plugins_1.loadPlugins)(process.cwd());
@@ -180,6 +192,10 @@ class Agent {
180
192
  }
181
193
  break;
182
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
+ }
183
199
  yield { type: 'usage', usage: event.usage };
184
200
  break;
185
201
  case 'error':
@@ -231,6 +247,11 @@ class Agent {
231
247
  yield { type: 'done' };
232
248
  return;
233
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
+ }
234
255
  const prepared = [];
235
256
  for (const tc of toolCalls) {
236
257
  const toolName = tc.function.name;
@@ -239,6 +260,13 @@ class Agent {
239
260
  prepared.push({ tc, tool: null, args: {}, denied: false, error: `Error: Unknown tool "${toolName}"` });
240
261
  continue;
241
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
+ }
242
270
  let args;
243
271
  try {
244
272
  args = JSON.parse(tc.function.arguments);
@@ -254,9 +282,11 @@ class Agent {
254
282
  continue;
255
283
  }
256
284
  yield { type: 'tool_call', toolCall: { name: toolName, args } };
257
- // Permission check (sequential needs user interaction)
258
- const needsPermission = tool.permission === 'always-ask' ||
259
- (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);
260
290
  let denied = false;
261
291
  if (needsPermission) {
262
292
  const approved = await this.askPermission(toolName, args);
@@ -310,6 +340,11 @@ class Agent {
310
340
  const output = await prep.tool.execute(prep.args);
311
341
  // Audit log: successful execution
312
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
+ }
313
348
  // Store in cache for cacheable tools
314
349
  if (prep.tool.cacheable) {
315
350
  const ttl = cache_1.ToolCache.TTL[toolName] || 30_000;
@@ -387,6 +422,18 @@ class Agent {
387
422
  getMessages() {
388
423
  return [...this.messages];
389
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
+ }
390
437
  /**
391
438
  * Validate and repair message history to prevent OpenAI 400 errors.
392
439
  * Handles three types of corruption:
package/dist/audit.d.ts CHANGED
@@ -1,39 +1,61 @@
1
1
  /**
2
- * Audit logger for CodeBot.
2
+ * Audit logger for CodeBot v1.7.0
3
3
  *
4
4
  * Provides append-only JSONL logging of all security-relevant actions.
5
5
  * Logs are stored at ~/.codebot/audit/audit-YYYY-MM-DD.jsonl
6
- * Masks secrets in args before writing.
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
+ *
7
11
  * NEVER throws — audit failures must not crash the agent.
8
12
  */
9
13
  export interface AuditEntry {
10
14
  timestamp: string;
11
15
  sessionId: string;
16
+ sequence: number;
12
17
  tool: string;
13
- action: 'execute' | 'deny' | 'error' | 'security_block';
18
+ action: 'execute' | 'deny' | 'error' | 'security_block' | 'policy_block';
14
19
  args: Record<string, unknown>;
15
20
  result?: string;
16
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;
17
31
  }
18
32
  export declare class AuditLogger {
19
33
  private logDir;
20
34
  private sessionId;
35
+ private sequence;
36
+ private prevHash;
21
37
  constructor(logDir?: string);
22
- /** Get the current session ID */
23
38
  getSessionId(): string;
24
- /** Append an audit entry to the log file */
25
- log(entry: Omit<AuditEntry, 'timestamp' | 'sessionId'>): void;
39
+ /** Append a hash-chained audit entry to the log file */
40
+ log(entry: Omit<AuditEntry, 'timestamp' | 'sessionId' | 'sequence' | 'prevHash' | 'hash'>): void;
26
41
  /** Read log entries, optionally filtered */
27
42
  query(filter?: {
28
43
  tool?: string;
29
44
  action?: string;
30
45
  since?: string;
46
+ sessionId?: string;
31
47
  }): AuditEntry[];
32
- /** Get the path to today's log file */
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;
33
57
  private getLogFilePath;
34
- /** Rotate log file if it exceeds MAX_LOG_SIZE */
35
58
  private rotateIfNeeded;
36
- /** Sanitize args for logging: mask secrets and truncate long values */
37
59
  private sanitizeArgs;
38
60
  }
39
61
  //# sourceMappingURL=audit.d.ts.map
package/dist/audit.js CHANGED
@@ -37,12 +37,16 @@ exports.AuditLogger = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const os = __importStar(require("os"));
40
+ const crypto = __importStar(require("crypto"));
40
41
  const secrets_1 = require("./secrets");
41
42
  const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB before rotation
42
- const MAX_ARG_LENGTH = 500; // Truncate long arg values for logging
43
+ const MAX_ARG_LENGTH = 500;
44
+ const GENESIS_HASH = 'genesis';
43
45
  class AuditLogger {
44
46
  logDir;
45
47
  sessionId;
48
+ sequence = 0;
49
+ prevHash = GENESIS_HASH;
46
50
  constructor(logDir) {
47
51
  this.logDir = logDir || path.join(os.homedir(), '.codebot', 'audit');
48
52
  this.sessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
@@ -53,22 +57,32 @@ class AuditLogger {
53
57
  // Can't create dir — logging will be disabled
54
58
  }
55
59
  }
56
- /** Get the current session ID */
57
60
  getSessionId() {
58
61
  return this.sessionId;
59
62
  }
60
- /** Append an audit entry to the log file */
63
+ /** Append a hash-chained audit entry to the log file */
61
64
  log(entry) {
62
65
  try {
63
- const fullEntry = {
66
+ this.sequence++;
67
+ // Build entry without hash first (hash is computed over the other fields)
68
+ const partial = {
64
69
  timestamp: new Date().toISOString(),
65
70
  sessionId: this.sessionId,
66
- ...entry,
71
+ sequence: this.sequence,
72
+ tool: entry.tool,
73
+ action: entry.action,
67
74
  args: this.sanitizeArgs(entry.args),
75
+ result: entry.result,
76
+ reason: entry.reason,
77
+ prevHash: this.prevHash,
68
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;
69
84
  const logFile = this.getLogFilePath();
70
85
  const line = JSON.stringify(fullEntry) + '\n';
71
- // Check if rotation is needed
72
86
  this.rotateIfNeeded(logFile);
73
87
  fs.appendFileSync(logFile, line, 'utf-8');
74
88
  }
@@ -96,6 +110,8 @@ class AuditLogger {
96
110
  continue;
97
111
  if (filter?.since && entry.timestamp < filter.since)
98
112
  continue;
113
+ if (filter?.sessionId && entry.sessionId !== filter.sessionId)
114
+ continue;
99
115
  entries.push(entry);
100
116
  }
101
117
  catch { /* skip malformed */ }
@@ -107,12 +123,72 @@ class AuditLogger {
107
123
  }
108
124
  return entries;
109
125
  }
110
- /** Get the path to today's log file */
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
+ }
111
188
  getLogFilePath() {
112
- const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
189
+ const date = new Date().toISOString().split('T')[0];
113
190
  return path.join(this.logDir, `audit-${date}.jsonl`);
114
191
  }
115
- /** Rotate log file if it exceeds MAX_LOG_SIZE */
116
192
  rotateIfNeeded(logFile) {
117
193
  try {
118
194
  if (!fs.existsSync(logFile))
@@ -127,7 +203,6 @@ class AuditLogger {
127
203
  // Rotation failure is non-fatal
128
204
  }
129
205
  }
130
- /** Sanitize args for logging: mask secrets and truncate long values */
131
206
  sanitizeArgs(args) {
132
207
  const sanitized = {};
133
208
  for (const [key, value] of Object.entries(args)) {
@@ -139,7 +214,6 @@ class AuditLogger {
139
214
  sanitized[key] = masked;
140
215
  }
141
216
  else if (typeof value === 'object' && value !== null) {
142
- // For objects/arrays, stringify and mask
143
217
  const str = JSON.stringify(value);
144
218
  const masked = (0, secrets_1.maskSecretsInString)(str);
145
219
  sanitized[key] = masked.length > MAX_ARG_LENGTH