codebot-ai 1.6.0 → 1.8.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 +22 -0
- package/dist/agent.js +138 -5
- package/dist/audit.d.ts +31 -9
- package/dist/audit.js +85 -11
- package/dist/capabilities.d.ts +48 -0
- package/dist/capabilities.js +187 -0
- package/dist/cli.js +265 -26
- package/dist/history.d.ts +7 -3
- package/dist/history.js +55 -8
- package/dist/index.d.ts +6 -0
- package/dist/index.js +13 -1
- package/dist/integrity.d.ts +35 -0
- package/dist/integrity.js +135 -0
- package/dist/policy.d.ts +132 -0
- package/dist/policy.js +444 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +4 -0
- package/dist/providers/openai.d.ts +1 -0
- package/dist/providers/openai.js +4 -0
- package/dist/replay.d.ts +55 -0
- package/dist/replay.js +196 -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/batch-edit.d.ts +3 -0
- package/dist/tools/batch-edit.js +12 -0
- package/dist/tools/edit.d.ts +3 -0
- package/dist/tools/edit.js +11 -0
- package/dist/tools/execute.js +29 -4
- package/dist/tools/git.d.ts +5 -0
- package/dist/tools/git.js +31 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +6 -6
- package/dist/tools/write.d.ts +3 -0
- package/dist/tools/write.js +11 -0
- package/dist/types.d.ts +1 -0
- 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,15 @@ export declare class Agent {
|
|
|
10
13
|
private cache;
|
|
11
14
|
private rateLimiter;
|
|
12
15
|
private auditLogger;
|
|
16
|
+
private policyEnforcer;
|
|
17
|
+
private tokenTracker;
|
|
18
|
+
private branchCreated;
|
|
13
19
|
private askPermission;
|
|
14
20
|
private onMessage?;
|
|
15
21
|
constructor(opts: {
|
|
16
22
|
provider: LLMProvider;
|
|
17
23
|
model: string;
|
|
24
|
+
providerName?: string;
|
|
18
25
|
maxIterations?: number;
|
|
19
26
|
autoApprove?: boolean;
|
|
20
27
|
askPermission?: (tool: string, args: Record<string, unknown>) => Promise<boolean>;
|
|
@@ -31,6 +38,12 @@ export declare class Agent {
|
|
|
31
38
|
after: number;
|
|
32
39
|
};
|
|
33
40
|
getMessages(): Message[];
|
|
41
|
+
/** Get the token tracker for session summary / CLI display */
|
|
42
|
+
getTokenTracker(): TokenTracker;
|
|
43
|
+
/** Get the policy enforcer for inspection */
|
|
44
|
+
getPolicyEnforcer(): PolicyEnforcer;
|
|
45
|
+
/** Get the audit logger for verification */
|
|
46
|
+
getAuditLogger(): AuditLogger;
|
|
34
47
|
/**
|
|
35
48
|
* Validate and repair message history to prevent OpenAI 400 errors.
|
|
36
49
|
* Handles three types of corruption:
|
|
@@ -42,6 +55,15 @@ export declare class Agent {
|
|
|
42
55
|
* or session resume corruption.
|
|
43
56
|
*/
|
|
44
57
|
private repairToolCallMessages;
|
|
58
|
+
/**
|
|
59
|
+
* Auto-create a feature branch when always_branch is enabled and on main/master.
|
|
60
|
+
* Called before the first write/edit operation. Fail-open: if branching fails, continue.
|
|
61
|
+
*/
|
|
62
|
+
private ensureBranch;
|
|
63
|
+
/** Sanitize user message into a branch-safe slug. */
|
|
64
|
+
private sanitizeSlug;
|
|
65
|
+
/** Check capability-based restrictions before tool execution. Returns reason string or null. */
|
|
66
|
+
private checkToolCapabilities;
|
|
45
67
|
private buildSystemPrompt;
|
|
46
68
|
}
|
|
47
69
|
//# sourceMappingURL=agent.d.ts.map
|
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,20 +102,31 @@ class Agent {
|
|
|
100
102
|
cache;
|
|
101
103
|
rateLimiter;
|
|
102
104
|
auditLogger;
|
|
105
|
+
policyEnforcer;
|
|
106
|
+
tokenTracker;
|
|
107
|
+
branchCreated = false;
|
|
103
108
|
askPermission;
|
|
104
109
|
onMessage;
|
|
105
110
|
constructor(opts) {
|
|
106
111
|
this.provider = opts.provider;
|
|
107
112
|
this.model = opts.model;
|
|
108
|
-
|
|
113
|
+
// Load policy FIRST — tools need it for filesystem/git enforcement
|
|
114
|
+
this.policyEnforcer = new policy_1.PolicyEnforcer((0, policy_1.loadPolicy)(process.cwd()), process.cwd());
|
|
115
|
+
this.tools = new tools_1.ToolRegistry(process.cwd(), this.policyEnforcer);
|
|
109
116
|
this.context = new manager_1.ContextManager(opts.model, opts.provider);
|
|
110
|
-
|
|
117
|
+
// Use policy-defined max iterations as default, CLI overrides
|
|
118
|
+
this.maxIterations = opts.maxIterations || this.policyEnforcer.getMaxIterations();
|
|
111
119
|
this.autoApprove = opts.autoApprove || false;
|
|
112
120
|
this.askPermission = opts.askPermission || defaultAskPermission;
|
|
113
121
|
this.onMessage = opts.onMessage;
|
|
114
122
|
this.cache = new cache_1.ToolCache();
|
|
115
123
|
this.rateLimiter = new rate_limiter_1.RateLimiter();
|
|
116
124
|
this.auditLogger = new audit_1.AuditLogger();
|
|
125
|
+
// Token & cost tracking
|
|
126
|
+
this.tokenTracker = new telemetry_1.TokenTracker(opts.model, opts.providerName || 'unknown');
|
|
127
|
+
const costLimit = this.policyEnforcer.getCostLimitUsd();
|
|
128
|
+
if (costLimit > 0)
|
|
129
|
+
this.tokenTracker.setCostLimit(costLimit);
|
|
117
130
|
// Load plugins
|
|
118
131
|
try {
|
|
119
132
|
const plugins = (0, plugins_1.loadPlugins)(process.cwd());
|
|
@@ -180,6 +193,10 @@ class Agent {
|
|
|
180
193
|
}
|
|
181
194
|
break;
|
|
182
195
|
case 'usage':
|
|
196
|
+
// Track tokens and cost
|
|
197
|
+
if (event.usage) {
|
|
198
|
+
this.tokenTracker.recordUsage(event.usage.inputTokens || 0, event.usage.outputTokens || 0);
|
|
199
|
+
}
|
|
183
200
|
yield { type: 'usage', usage: event.usage };
|
|
184
201
|
break;
|
|
185
202
|
case 'error':
|
|
@@ -231,6 +248,11 @@ class Agent {
|
|
|
231
248
|
yield { type: 'done' };
|
|
232
249
|
return;
|
|
233
250
|
}
|
|
251
|
+
// Cost budget check: stop if over limit
|
|
252
|
+
if (this.tokenTracker.isOverBudget()) {
|
|
253
|
+
yield { type: 'error', error: `Cost limit exceeded ($${this.tokenTracker.getTotalCost().toFixed(4)} / $${this.policyEnforcer.getCostLimitUsd().toFixed(2)}). Stopping.` };
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
234
256
|
const prepared = [];
|
|
235
257
|
for (const tc of toolCalls) {
|
|
236
258
|
const toolName = tc.function.name;
|
|
@@ -239,6 +261,13 @@ class Agent {
|
|
|
239
261
|
prepared.push({ tc, tool: null, args: {}, denied: false, error: `Error: Unknown tool "${toolName}"` });
|
|
240
262
|
continue;
|
|
241
263
|
}
|
|
264
|
+
// Policy check: is this tool allowed?
|
|
265
|
+
const policyCheck = this.policyEnforcer.isToolAllowed(toolName);
|
|
266
|
+
if (!policyCheck.allowed) {
|
|
267
|
+
this.auditLogger.log({ tool: toolName, action: 'policy_block', args: {}, reason: policyCheck.reason });
|
|
268
|
+
prepared.push({ tc, tool, args: {}, denied: false, error: `Error: ${policyCheck.reason}` });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
242
271
|
let args;
|
|
243
272
|
try {
|
|
244
273
|
args = JSON.parse(tc.function.arguments);
|
|
@@ -254,9 +283,11 @@ class Agent {
|
|
|
254
283
|
continue;
|
|
255
284
|
}
|
|
256
285
|
yield { type: 'tool_call', toolCall: { name: toolName, args } };
|
|
257
|
-
// Permission check
|
|
258
|
-
const
|
|
259
|
-
|
|
286
|
+
// Permission check: policy override > tool default
|
|
287
|
+
const policyPermission = this.policyEnforcer.getToolPermission(toolName);
|
|
288
|
+
const effectivePermission = policyPermission || tool.permission;
|
|
289
|
+
const needsPermission = effectivePermission === 'always-ask' ||
|
|
290
|
+
(effectivePermission === 'prompt' && !this.autoApprove);
|
|
260
291
|
let denied = false;
|
|
261
292
|
if (needsPermission) {
|
|
262
293
|
const approved = await this.askPermission(toolName, args);
|
|
@@ -296,6 +327,19 @@ class Agent {
|
|
|
296
327
|
// Helper to execute a single tool with cache + rate limiting
|
|
297
328
|
const executeTool = async (prep) => {
|
|
298
329
|
const toolName = prep.tc.function.name;
|
|
330
|
+
// Auto-branch on first write/edit when always_branch is enabled (v1.8.0)
|
|
331
|
+
if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') {
|
|
332
|
+
const branchName = await this.ensureBranch();
|
|
333
|
+
if (branchName) {
|
|
334
|
+
this.auditLogger.log({ tool: 'git', action: 'execute', args: { branch: branchName }, result: 'auto-branch' });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Capability check: fine-grained resource restrictions (v1.8.0)
|
|
338
|
+
const capBlock = this.checkToolCapabilities(toolName, prep.args);
|
|
339
|
+
if (capBlock) {
|
|
340
|
+
this.auditLogger.log({ tool: toolName, action: 'capability_block', args: prep.args, reason: capBlock });
|
|
341
|
+
return { content: `Error: ${capBlock}`, is_error: true };
|
|
342
|
+
}
|
|
299
343
|
// Check cache first
|
|
300
344
|
if (prep.tool.cacheable) {
|
|
301
345
|
const cacheKey = cache_1.ToolCache.key(toolName, prep.args);
|
|
@@ -310,6 +354,11 @@ class Agent {
|
|
|
310
354
|
const output = await prep.tool.execute(prep.args);
|
|
311
355
|
// Audit log: successful execution
|
|
312
356
|
this.auditLogger.log({ tool: toolName, action: 'execute', args: prep.args, result: 'success' });
|
|
357
|
+
// Telemetry: track tool calls and file modifications
|
|
358
|
+
this.tokenTracker.recordToolCall();
|
|
359
|
+
if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && prep.args.path) {
|
|
360
|
+
this.tokenTracker.recordFileModified(prep.args.path);
|
|
361
|
+
}
|
|
313
362
|
// Store in cache for cacheable tools
|
|
314
363
|
if (prep.tool.cacheable) {
|
|
315
364
|
const ttl = cache_1.ToolCache.TTL[toolName] || 30_000;
|
|
@@ -387,6 +436,18 @@ class Agent {
|
|
|
387
436
|
getMessages() {
|
|
388
437
|
return [...this.messages];
|
|
389
438
|
}
|
|
439
|
+
/** Get the token tracker for session summary / CLI display */
|
|
440
|
+
getTokenTracker() {
|
|
441
|
+
return this.tokenTracker;
|
|
442
|
+
}
|
|
443
|
+
/** Get the policy enforcer for inspection */
|
|
444
|
+
getPolicyEnforcer() {
|
|
445
|
+
return this.policyEnforcer;
|
|
446
|
+
}
|
|
447
|
+
/** Get the audit logger for verification */
|
|
448
|
+
getAuditLogger() {
|
|
449
|
+
return this.auditLogger;
|
|
450
|
+
}
|
|
390
451
|
/**
|
|
391
452
|
* Validate and repair message history to prevent OpenAI 400 errors.
|
|
392
453
|
* Handles three types of corruption:
|
|
@@ -454,6 +515,78 @@ class Agent {
|
|
|
454
515
|
}
|
|
455
516
|
}
|
|
456
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Auto-create a feature branch when always_branch is enabled and on main/master.
|
|
520
|
+
* Called before the first write/edit operation. Fail-open: if branching fails, continue.
|
|
521
|
+
*/
|
|
522
|
+
async ensureBranch() {
|
|
523
|
+
if (this.branchCreated)
|
|
524
|
+
return null;
|
|
525
|
+
if (!this.policyEnforcer.shouldAlwaysBranch())
|
|
526
|
+
return null;
|
|
527
|
+
try {
|
|
528
|
+
const { execSync } = require('child_process');
|
|
529
|
+
const cwd = process.cwd();
|
|
530
|
+
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
531
|
+
cwd, encoding: 'utf-8', timeout: 5000,
|
|
532
|
+
}).trim();
|
|
533
|
+
if (currentBranch !== 'main' && currentBranch !== 'master') {
|
|
534
|
+
this.branchCreated = true;
|
|
535
|
+
return null; // Already on a feature branch
|
|
536
|
+
}
|
|
537
|
+
// Generate branch name from first user message
|
|
538
|
+
const firstUserMsg = this.messages.find(m => m.role === 'user');
|
|
539
|
+
const prefix = this.policyEnforcer.getBranchPrefix();
|
|
540
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
541
|
+
const slug = this.sanitizeSlug(firstUserMsg?.content || 'task');
|
|
542
|
+
const branchName = `${prefix}${timestamp}-${slug}`;
|
|
543
|
+
execSync(`git checkout -b "${branchName}"`, {
|
|
544
|
+
cwd, encoding: 'utf-8', timeout: 10000,
|
|
545
|
+
});
|
|
546
|
+
this.branchCreated = true;
|
|
547
|
+
return branchName;
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
// Don't block the operation if branching fails (not in a git repo, etc.)
|
|
551
|
+
this.branchCreated = true; // Don't retry
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/** Sanitize user message into a branch-safe slug. */
|
|
556
|
+
sanitizeSlug(message) {
|
|
557
|
+
return message
|
|
558
|
+
.toLowerCase()
|
|
559
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
560
|
+
.replace(/\s+/g, '-')
|
|
561
|
+
.substring(0, 30)
|
|
562
|
+
.replace(/-+$/, '') || 'task';
|
|
563
|
+
}
|
|
564
|
+
/** Check capability-based restrictions before tool execution. Returns reason string or null. */
|
|
565
|
+
checkToolCapabilities(toolName, args) {
|
|
566
|
+
// fs_write check for write/edit tools
|
|
567
|
+
if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && args.path) {
|
|
568
|
+
const check = this.policyEnforcer.checkCapability(toolName, 'fs_write', args.path);
|
|
569
|
+
if (!check.allowed)
|
|
570
|
+
return check.reason || 'Capability blocked';
|
|
571
|
+
}
|
|
572
|
+
// shell_commands check for execute tool
|
|
573
|
+
if (toolName === 'execute' && args.command) {
|
|
574
|
+
const check = this.policyEnforcer.checkCapability(toolName, 'shell_commands', args.command);
|
|
575
|
+
if (!check.allowed)
|
|
576
|
+
return check.reason || 'Capability blocked';
|
|
577
|
+
}
|
|
578
|
+
// net_access check for web tools
|
|
579
|
+
if ((toolName === 'web_fetch' || toolName === 'http_client') && args.url) {
|
|
580
|
+
try {
|
|
581
|
+
const domain = new URL(args.url).hostname;
|
|
582
|
+
const check = this.policyEnforcer.checkCapability(toolName, 'net_access', domain);
|
|
583
|
+
if (!check.allowed)
|
|
584
|
+
return check.reason || 'Capability blocked';
|
|
585
|
+
}
|
|
586
|
+
catch { /* invalid URL handled by the tool itself */ }
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
457
590
|
buildSystemPrompt(supportsTools) {
|
|
458
591
|
let repoMap = '';
|
|
459
592
|
try {
|
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' | 'capability_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
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability-Based Tool Permissions for CodeBot v1.8.0
|
|
3
|
+
*
|
|
4
|
+
* Fine-grained, per-tool resource restrictions.
|
|
5
|
+
* Configured via .codebot/policy.json → tools.capabilities.
|
|
6
|
+
*
|
|
7
|
+
* Example:
|
|
8
|
+
* {
|
|
9
|
+
* "execute": {
|
|
10
|
+
* "shell_commands": ["npm", "node", "git", "tsc"],
|
|
11
|
+
* "max_output_kb": 500
|
|
12
|
+
* },
|
|
13
|
+
* "write_file": {
|
|
14
|
+
* "fs_write": ["./src/**", "./tests/**"]
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
export interface ToolCapabilities {
|
|
19
|
+
fs_read?: string[];
|
|
20
|
+
fs_write?: string[];
|
|
21
|
+
net_access?: string[];
|
|
22
|
+
shell_commands?: string[];
|
|
23
|
+
max_output_kb?: number;
|
|
24
|
+
}
|
|
25
|
+
export type CapabilityConfig = Record<string, ToolCapabilities>;
|
|
26
|
+
export declare class CapabilityChecker {
|
|
27
|
+
private capabilities;
|
|
28
|
+
private projectRoot;
|
|
29
|
+
constructor(capabilities: CapabilityConfig, projectRoot: string);
|
|
30
|
+
/** Get capabilities for a tool. undefined = no restrictions. */
|
|
31
|
+
getToolCapabilities(toolName: string): ToolCapabilities | undefined;
|
|
32
|
+
/** Check if a specific capability is allowed. */
|
|
33
|
+
checkCapability(toolName: string, capabilityType: keyof ToolCapabilities, value: string | number): {
|
|
34
|
+
allowed: boolean;
|
|
35
|
+
reason?: string;
|
|
36
|
+
};
|
|
37
|
+
/** Check if a path matches any of the allowed glob patterns. */
|
|
38
|
+
private checkGlobs;
|
|
39
|
+
/** Check if a domain is in the allowed list. */
|
|
40
|
+
private checkDomain;
|
|
41
|
+
/** Check if a command starts with an allowed prefix. */
|
|
42
|
+
private checkCommandPrefix;
|
|
43
|
+
/** Check if output size is within the cap. */
|
|
44
|
+
private checkOutputSize;
|
|
45
|
+
/** Simple glob matching (** = any depth, * = one segment). */
|
|
46
|
+
private matchGlob;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=capabilities.d.ts.map
|