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 +12 -0
- package/dist/agent.js +51 -4
- package/dist/audit.d.ts +31 -9
- package/dist/audit.js +85 -11
- package/dist/cli.js +181 -26
- 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/telemetry.d.ts +73 -0
- package/dist/telemetry.js +286 -0
- package/dist/tools/execute.js +29 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
|
258
|
-
const
|
|
259
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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;
|
|
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
|
|
63
|
+
/** Append a hash-chained audit entry to the log file */
|
|
61
64
|
log(entry) {
|
|
62
65
|
try {
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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];
|
|
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
|