codebot-ai 1.7.0 → 1.9.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 +18 -0
- package/dist/agent.js +135 -6
- package/dist/audit.d.ts +1 -1
- package/dist/capabilities.d.ts +48 -0
- package/dist/capabilities.js +187 -0
- package/dist/cli.js +167 -4
- package/dist/history.d.ts +7 -3
- package/dist/history.js +55 -8
- package/dist/index.d.ts +12 -0
- package/dist/index.js +20 -1
- package/dist/integrity.d.ts +35 -0
- package/dist/integrity.js +135 -0
- package/dist/metrics.d.ts +60 -0
- package/dist/metrics.js +296 -0
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +32 -6
- 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/risk.d.ts +52 -0
- package/dist/risk.js +367 -0
- package/dist/sarif.d.ts +82 -0
- package/dist/sarif.js +176 -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/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 +5 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Message, AgentEvent, LLMProvider } from './types';
|
|
|
2
2
|
import { AuditLogger } from './audit';
|
|
3
3
|
import { PolicyEnforcer } from './policy';
|
|
4
4
|
import { TokenTracker } from './telemetry';
|
|
5
|
+
import { MetricsCollector } from './metrics';
|
|
6
|
+
import { RiskScorer } from './risk';
|
|
5
7
|
export declare class Agent {
|
|
6
8
|
private provider;
|
|
7
9
|
private tools;
|
|
@@ -15,6 +17,9 @@ export declare class Agent {
|
|
|
15
17
|
private auditLogger;
|
|
16
18
|
private policyEnforcer;
|
|
17
19
|
private tokenTracker;
|
|
20
|
+
private metricsCollector;
|
|
21
|
+
private riskScorer;
|
|
22
|
+
private branchCreated;
|
|
18
23
|
private askPermission;
|
|
19
24
|
private onMessage?;
|
|
20
25
|
constructor(opts: {
|
|
@@ -43,6 +48,10 @@ export declare class Agent {
|
|
|
43
48
|
getPolicyEnforcer(): PolicyEnforcer;
|
|
44
49
|
/** Get the audit logger for verification */
|
|
45
50
|
getAuditLogger(): AuditLogger;
|
|
51
|
+
/** Get the metrics collector for session metrics */
|
|
52
|
+
getMetrics(): MetricsCollector;
|
|
53
|
+
/** Get the risk scorer for risk assessment history */
|
|
54
|
+
getRiskScorer(): RiskScorer;
|
|
46
55
|
/**
|
|
47
56
|
* Validate and repair message history to prevent OpenAI 400 errors.
|
|
48
57
|
* Handles three types of corruption:
|
|
@@ -54,6 +63,15 @@ export declare class Agent {
|
|
|
54
63
|
* or session resume corruption.
|
|
55
64
|
*/
|
|
56
65
|
private repairToolCallMessages;
|
|
66
|
+
/**
|
|
67
|
+
* Auto-create a feature branch when always_branch is enabled and on main/master.
|
|
68
|
+
* Called before the first write/edit operation. Fail-open: if branching fails, continue.
|
|
69
|
+
*/
|
|
70
|
+
private ensureBranch;
|
|
71
|
+
/** Sanitize user message into a branch-safe slug. */
|
|
72
|
+
private sanitizeSlug;
|
|
73
|
+
/** Check capability-based restrictions before tool execution. Returns reason string or null. */
|
|
74
|
+
private checkToolCapabilities;
|
|
57
75
|
private buildSystemPrompt;
|
|
58
76
|
}
|
|
59
77
|
//# sourceMappingURL=agent.d.ts.map
|
package/dist/agent.js
CHANGED
|
@@ -48,6 +48,8 @@ const rate_limiter_1 = require("./rate-limiter");
|
|
|
48
48
|
const audit_1 = require("./audit");
|
|
49
49
|
const policy_1 = require("./policy");
|
|
50
50
|
const telemetry_1 = require("./telemetry");
|
|
51
|
+
const metrics_1 = require("./metrics");
|
|
52
|
+
const risk_1 = require("./risk");
|
|
51
53
|
/** Lightweight schema validation — returns error string or null if valid */
|
|
52
54
|
function validateToolArgs(args, schema) {
|
|
53
55
|
const props = schema.properties;
|
|
@@ -104,15 +106,18 @@ class Agent {
|
|
|
104
106
|
auditLogger;
|
|
105
107
|
policyEnforcer;
|
|
106
108
|
tokenTracker;
|
|
109
|
+
metricsCollector;
|
|
110
|
+
riskScorer;
|
|
111
|
+
branchCreated = false;
|
|
107
112
|
askPermission;
|
|
108
113
|
onMessage;
|
|
109
114
|
constructor(opts) {
|
|
110
115
|
this.provider = opts.provider;
|
|
111
116
|
this.model = opts.model;
|
|
112
|
-
|
|
113
|
-
this.context = new manager_1.ContextManager(opts.model, opts.provider);
|
|
114
|
-
// Load policy
|
|
117
|
+
// Load policy FIRST — tools need it for filesystem/git enforcement
|
|
115
118
|
this.policyEnforcer = new policy_1.PolicyEnforcer((0, policy_1.loadPolicy)(process.cwd()), process.cwd());
|
|
119
|
+
this.tools = new tools_1.ToolRegistry(process.cwd(), this.policyEnforcer);
|
|
120
|
+
this.context = new manager_1.ContextManager(opts.model, opts.provider);
|
|
116
121
|
// Use policy-defined max iterations as default, CLI overrides
|
|
117
122
|
this.maxIterations = opts.maxIterations || this.policyEnforcer.getMaxIterations();
|
|
118
123
|
this.autoApprove = opts.autoApprove || false;
|
|
@@ -123,6 +128,8 @@ class Agent {
|
|
|
123
128
|
this.auditLogger = new audit_1.AuditLogger();
|
|
124
129
|
// Token & cost tracking
|
|
125
130
|
this.tokenTracker = new telemetry_1.TokenTracker(opts.model, opts.providerName || 'unknown');
|
|
131
|
+
this.metricsCollector = new metrics_1.MetricsCollector();
|
|
132
|
+
this.riskScorer = new risk_1.RiskScorer();
|
|
126
133
|
const costLimit = this.policyEnforcer.getCostLimitUsd();
|
|
127
134
|
if (costLimit > 0)
|
|
128
135
|
this.tokenTracker.setCostLimit(costLimit);
|
|
@@ -195,6 +202,9 @@ class Agent {
|
|
|
195
202
|
// Track tokens and cost
|
|
196
203
|
if (event.usage) {
|
|
197
204
|
this.tokenTracker.recordUsage(event.usage.inputTokens || 0, event.usage.outputTokens || 0);
|
|
205
|
+
this.metricsCollector.increment('llm_requests_total');
|
|
206
|
+
this.metricsCollector.increment('llm_tokens_total', { direction: 'input' }, event.usage.inputTokens || 0);
|
|
207
|
+
this.metricsCollector.increment('llm_tokens_total', { direction: 'output' }, event.usage.outputTokens || 0);
|
|
198
208
|
}
|
|
199
209
|
yield { type: 'usage', usage: event.usage };
|
|
200
210
|
break;
|
|
@@ -281,10 +291,17 @@ class Agent {
|
|
|
281
291
|
prepared.push({ tc, tool, args, denied: false, error: `Error: ${validationError} for ${toolName}` });
|
|
282
292
|
continue;
|
|
283
293
|
}
|
|
284
|
-
|
|
285
|
-
// Permission check: policy override > tool default
|
|
294
|
+
// Compute risk score before execution
|
|
286
295
|
const policyPermission = this.policyEnforcer.getToolPermission(toolName);
|
|
287
296
|
const effectivePermission = policyPermission || tool.permission;
|
|
297
|
+
const riskAssessment = this.riskScorer.assess(toolName, args, effectivePermission);
|
|
298
|
+
yield { type: 'tool_call', toolCall: { name: toolName, args }, risk: { score: riskAssessment.score, level: riskAssessment.level } };
|
|
299
|
+
// Log risk breakdown for high-risk calls
|
|
300
|
+
if (riskAssessment.score > 50) {
|
|
301
|
+
const breakdown = riskAssessment.factors.map(f => `${f.name}=${f.rawScore}`).join(', ');
|
|
302
|
+
this.auditLogger.log({ tool: toolName, action: 'execute', args, result: `risk:${riskAssessment.score}`, reason: breakdown });
|
|
303
|
+
}
|
|
304
|
+
// Permission check: policy override > tool default
|
|
288
305
|
const needsPermission = effectivePermission === 'always-ask' ||
|
|
289
306
|
(effectivePermission === 'prompt' && !this.autoApprove);
|
|
290
307
|
let denied = false;
|
|
@@ -293,6 +310,7 @@ class Agent {
|
|
|
293
310
|
if (!approved) {
|
|
294
311
|
denied = true;
|
|
295
312
|
this.auditLogger.log({ tool: toolName, action: 'deny', args, reason: 'User denied permission' });
|
|
313
|
+
this.metricsCollector.increment('permission_denials_total', { tool: toolName });
|
|
296
314
|
}
|
|
297
315
|
}
|
|
298
316
|
prepared.push({ tc, tool, args, denied });
|
|
@@ -323,27 +341,53 @@ class Agent {
|
|
|
323
341
|
parallelBatch.push(item);
|
|
324
342
|
}
|
|
325
343
|
}
|
|
326
|
-
// Helper to execute a single tool with cache + rate limiting
|
|
344
|
+
// Helper to execute a single tool with cache + rate limiting + metrics
|
|
327
345
|
const executeTool = async (prep) => {
|
|
328
346
|
const toolName = prep.tc.function.name;
|
|
347
|
+
const toolStartTime = Date.now();
|
|
348
|
+
// Auto-branch on first write/edit when always_branch is enabled (v1.8.0)
|
|
349
|
+
if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') {
|
|
350
|
+
const branchName = await this.ensureBranch();
|
|
351
|
+
if (branchName) {
|
|
352
|
+
this.auditLogger.log({ tool: 'git', action: 'execute', args: { branch: branchName }, result: 'auto-branch' });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Capability check: fine-grained resource restrictions (v1.8.0)
|
|
356
|
+
const capBlock = this.checkToolCapabilities(toolName, prep.args);
|
|
357
|
+
if (capBlock) {
|
|
358
|
+
this.auditLogger.log({ tool: toolName, action: 'capability_block', args: prep.args, reason: capBlock });
|
|
359
|
+
this.metricsCollector.increment('security_blocks_total', { tool: toolName, type: 'capability' });
|
|
360
|
+
return { content: `Error: ${capBlock}`, is_error: true };
|
|
361
|
+
}
|
|
329
362
|
// Check cache first
|
|
330
363
|
if (prep.tool.cacheable) {
|
|
331
364
|
const cacheKey = cache_1.ToolCache.key(toolName, prep.args);
|
|
332
365
|
const cached = this.cache.get(cacheKey);
|
|
333
366
|
if (cached !== null) {
|
|
367
|
+
this.metricsCollector.increment('cache_hits_total', { tool: toolName });
|
|
334
368
|
return { content: cached };
|
|
335
369
|
}
|
|
370
|
+
this.metricsCollector.increment('cache_misses_total', { tool: toolName });
|
|
336
371
|
}
|
|
337
372
|
// Rate limit
|
|
338
373
|
await this.rateLimiter.throttle(toolName);
|
|
339
374
|
try {
|
|
340
375
|
const output = await prep.tool.execute(prep.args);
|
|
376
|
+
// Record tool latency
|
|
377
|
+
const latencyMs = Date.now() - toolStartTime;
|
|
378
|
+
this.metricsCollector.observe('tool_latency_seconds', latencyMs / 1000, { tool: toolName });
|
|
379
|
+
this.metricsCollector.increment('tool_calls_total', { tool: toolName });
|
|
341
380
|
// Audit log: successful execution
|
|
342
381
|
this.auditLogger.log({ tool: toolName, action: 'execute', args: prep.args, result: 'success' });
|
|
343
382
|
// Telemetry: track tool calls and file modifications
|
|
344
383
|
this.tokenTracker.recordToolCall();
|
|
345
384
|
if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && prep.args.path) {
|
|
346
385
|
this.tokenTracker.recordFileModified(prep.args.path);
|
|
386
|
+
this.metricsCollector.increment('files_written_total', { tool: toolName });
|
|
387
|
+
}
|
|
388
|
+
// Track commands executed
|
|
389
|
+
if (toolName === 'execute') {
|
|
390
|
+
this.metricsCollector.increment('commands_executed_total');
|
|
347
391
|
}
|
|
348
392
|
// Store in cache for cacheable tools
|
|
349
393
|
if (prep.tool.cacheable) {
|
|
@@ -359,11 +403,16 @@ class Agent {
|
|
|
359
403
|
// Audit log: check if tool returned a security block
|
|
360
404
|
if (output.startsWith('Error: Blocked:') || output.startsWith('Error: CWD')) {
|
|
361
405
|
this.auditLogger.log({ tool: toolName, action: 'security_block', args: prep.args, reason: output });
|
|
406
|
+
this.metricsCollector.increment('security_blocks_total', { tool: toolName, type: 'security' });
|
|
362
407
|
}
|
|
363
408
|
return { content: output };
|
|
364
409
|
}
|
|
365
410
|
catch (err) {
|
|
366
411
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
412
|
+
// Record latency even on error
|
|
413
|
+
const latencyMs = Date.now() - toolStartTime;
|
|
414
|
+
this.metricsCollector.observe('tool_latency_seconds', latencyMs / 1000, { tool: toolName });
|
|
415
|
+
this.metricsCollector.increment('errors_total', { tool: toolName });
|
|
367
416
|
// Audit log: error
|
|
368
417
|
this.auditLogger.log({ tool: toolName, action: 'error', args: prep.args, result: 'error', reason: errMsg });
|
|
369
418
|
return { content: `Error: ${errMsg}`, is_error: true };
|
|
@@ -434,6 +483,14 @@ class Agent {
|
|
|
434
483
|
getAuditLogger() {
|
|
435
484
|
return this.auditLogger;
|
|
436
485
|
}
|
|
486
|
+
/** Get the metrics collector for session metrics */
|
|
487
|
+
getMetrics() {
|
|
488
|
+
return this.metricsCollector;
|
|
489
|
+
}
|
|
490
|
+
/** Get the risk scorer for risk assessment history */
|
|
491
|
+
getRiskScorer() {
|
|
492
|
+
return this.riskScorer;
|
|
493
|
+
}
|
|
437
494
|
/**
|
|
438
495
|
* Validate and repair message history to prevent OpenAI 400 errors.
|
|
439
496
|
* Handles three types of corruption:
|
|
@@ -501,6 +558,78 @@ class Agent {
|
|
|
501
558
|
}
|
|
502
559
|
}
|
|
503
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* Auto-create a feature branch when always_branch is enabled and on main/master.
|
|
563
|
+
* Called before the first write/edit operation. Fail-open: if branching fails, continue.
|
|
564
|
+
*/
|
|
565
|
+
async ensureBranch() {
|
|
566
|
+
if (this.branchCreated)
|
|
567
|
+
return null;
|
|
568
|
+
if (!this.policyEnforcer.shouldAlwaysBranch())
|
|
569
|
+
return null;
|
|
570
|
+
try {
|
|
571
|
+
const { execSync } = require('child_process');
|
|
572
|
+
const cwd = process.cwd();
|
|
573
|
+
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
574
|
+
cwd, encoding: 'utf-8', timeout: 5000,
|
|
575
|
+
}).trim();
|
|
576
|
+
if (currentBranch !== 'main' && currentBranch !== 'master') {
|
|
577
|
+
this.branchCreated = true;
|
|
578
|
+
return null; // Already on a feature branch
|
|
579
|
+
}
|
|
580
|
+
// Generate branch name from first user message
|
|
581
|
+
const firstUserMsg = this.messages.find(m => m.role === 'user');
|
|
582
|
+
const prefix = this.policyEnforcer.getBranchPrefix();
|
|
583
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
584
|
+
const slug = this.sanitizeSlug(firstUserMsg?.content || 'task');
|
|
585
|
+
const branchName = `${prefix}${timestamp}-${slug}`;
|
|
586
|
+
execSync(`git checkout -b "${branchName}"`, {
|
|
587
|
+
cwd, encoding: 'utf-8', timeout: 10000,
|
|
588
|
+
});
|
|
589
|
+
this.branchCreated = true;
|
|
590
|
+
return branchName;
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// Don't block the operation if branching fails (not in a git repo, etc.)
|
|
594
|
+
this.branchCreated = true; // Don't retry
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/** Sanitize user message into a branch-safe slug. */
|
|
599
|
+
sanitizeSlug(message) {
|
|
600
|
+
return message
|
|
601
|
+
.toLowerCase()
|
|
602
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
603
|
+
.replace(/\s+/g, '-')
|
|
604
|
+
.substring(0, 30)
|
|
605
|
+
.replace(/-+$/, '') || 'task';
|
|
606
|
+
}
|
|
607
|
+
/** Check capability-based restrictions before tool execution. Returns reason string or null. */
|
|
608
|
+
checkToolCapabilities(toolName, args) {
|
|
609
|
+
// fs_write check for write/edit tools
|
|
610
|
+
if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && args.path) {
|
|
611
|
+
const check = this.policyEnforcer.checkCapability(toolName, 'fs_write', args.path);
|
|
612
|
+
if (!check.allowed)
|
|
613
|
+
return check.reason || 'Capability blocked';
|
|
614
|
+
}
|
|
615
|
+
// shell_commands check for execute tool
|
|
616
|
+
if (toolName === 'execute' && args.command) {
|
|
617
|
+
const check = this.policyEnforcer.checkCapability(toolName, 'shell_commands', args.command);
|
|
618
|
+
if (!check.allowed)
|
|
619
|
+
return check.reason || 'Capability blocked';
|
|
620
|
+
}
|
|
621
|
+
// net_access check for web tools
|
|
622
|
+
if ((toolName === 'web_fetch' || toolName === 'http_client') && args.url) {
|
|
623
|
+
try {
|
|
624
|
+
const domain = new URL(args.url).hostname;
|
|
625
|
+
const check = this.policyEnforcer.checkCapability(toolName, 'net_access', domain);
|
|
626
|
+
if (!check.allowed)
|
|
627
|
+
return check.reason || 'Capability blocked';
|
|
628
|
+
}
|
|
629
|
+
catch { /* invalid URL handled by the tool itself */ }
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
504
633
|
buildSystemPrompt(supportsTools) {
|
|
505
634
|
let repoMap = '';
|
|
506
635
|
try {
|
package/dist/audit.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface AuditEntry {
|
|
|
15
15
|
sessionId: string;
|
|
16
16
|
sequence: number;
|
|
17
17
|
tool: string;
|
|
18
|
-
action: 'execute' | 'deny' | 'error' | 'security_block' | 'policy_block';
|
|
18
|
+
action: 'execute' | 'deny' | 'error' | 'security_block' | 'policy_block' | 'capability_block';
|
|
19
19
|
args: Record<string, unknown>;
|
|
20
20
|
result?: string;
|
|
21
21
|
reason?: string;
|
|
@@ -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
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Capability-Based Tool Permissions for CodeBot v1.8.0
|
|
4
|
+
*
|
|
5
|
+
* Fine-grained, per-tool resource restrictions.
|
|
6
|
+
* Configured via .codebot/policy.json → tools.capabilities.
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* {
|
|
10
|
+
* "execute": {
|
|
11
|
+
* "shell_commands": ["npm", "node", "git", "tsc"],
|
|
12
|
+
* "max_output_kb": 500
|
|
13
|
+
* },
|
|
14
|
+
* "write_file": {
|
|
15
|
+
* "fs_write": ["./src/**", "./tests/**"]
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.CapabilityChecker = void 0;
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
// ── Capability Checker ──
|
|
56
|
+
class CapabilityChecker {
|
|
57
|
+
capabilities;
|
|
58
|
+
projectRoot;
|
|
59
|
+
constructor(capabilities, projectRoot) {
|
|
60
|
+
this.capabilities = capabilities;
|
|
61
|
+
this.projectRoot = projectRoot;
|
|
62
|
+
}
|
|
63
|
+
/** Get capabilities for a tool. undefined = no restrictions. */
|
|
64
|
+
getToolCapabilities(toolName) {
|
|
65
|
+
return this.capabilities[toolName];
|
|
66
|
+
}
|
|
67
|
+
/** Check if a specific capability is allowed. */
|
|
68
|
+
checkCapability(toolName, capabilityType, value) {
|
|
69
|
+
const caps = this.capabilities[toolName];
|
|
70
|
+
if (!caps)
|
|
71
|
+
return { allowed: true }; // No caps defined = unrestricted
|
|
72
|
+
switch (capabilityType) {
|
|
73
|
+
case 'fs_read':
|
|
74
|
+
return this.checkGlobs(caps.fs_read, value, 'fs_read', toolName);
|
|
75
|
+
case 'fs_write':
|
|
76
|
+
return this.checkGlobs(caps.fs_write, value, 'fs_write', toolName);
|
|
77
|
+
case 'net_access':
|
|
78
|
+
return this.checkDomain(caps.net_access, value, toolName);
|
|
79
|
+
case 'shell_commands':
|
|
80
|
+
return this.checkCommandPrefix(caps.shell_commands, value, toolName);
|
|
81
|
+
case 'max_output_kb':
|
|
82
|
+
return this.checkOutputSize(caps.max_output_kb, value, toolName);
|
|
83
|
+
default:
|
|
84
|
+
return { allowed: true };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Check if a path matches any of the allowed glob patterns. */
|
|
88
|
+
checkGlobs(patterns, filePath, capName, toolName) {
|
|
89
|
+
if (!patterns || patterns.length === 0)
|
|
90
|
+
return { allowed: true };
|
|
91
|
+
const resolved = path.resolve(filePath);
|
|
92
|
+
const relative = path.relative(this.projectRoot, resolved);
|
|
93
|
+
// Don't restrict paths outside the project (those are handled by security.ts)
|
|
94
|
+
if (relative.startsWith('..'))
|
|
95
|
+
return { allowed: true };
|
|
96
|
+
for (const pattern of patterns) {
|
|
97
|
+
if (this.matchGlob(relative, pattern)) {
|
|
98
|
+
return { allowed: true };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
allowed: false,
|
|
103
|
+
reason: `Tool "${toolName}" ${capName} capability blocks "${relative}" (allowed: ${patterns.join(', ')})`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Check if a domain is in the allowed list. */
|
|
107
|
+
checkDomain(allowed, domain, toolName) {
|
|
108
|
+
if (allowed === undefined)
|
|
109
|
+
return { allowed: true }; // undefined = unrestricted
|
|
110
|
+
if (allowed.length === 0) {
|
|
111
|
+
// Empty array = no network access allowed
|
|
112
|
+
return {
|
|
113
|
+
allowed: false,
|
|
114
|
+
reason: `Tool "${toolName}" has no allowed network domains`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (allowed.includes('*'))
|
|
118
|
+
return { allowed: true };
|
|
119
|
+
const normalizedDomain = domain.toLowerCase();
|
|
120
|
+
for (const d of allowed) {
|
|
121
|
+
const nd = d.toLowerCase();
|
|
122
|
+
if (normalizedDomain === nd)
|
|
123
|
+
return { allowed: true };
|
|
124
|
+
if (normalizedDomain.endsWith('.' + nd))
|
|
125
|
+
return { allowed: true };
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
allowed: false,
|
|
129
|
+
reason: `Tool "${toolName}" cannot access domain "${domain}" (allowed: ${allowed.join(', ')})`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Check if a command starts with an allowed prefix. */
|
|
133
|
+
checkCommandPrefix(allowed, command, toolName) {
|
|
134
|
+
if (!allowed || allowed.length === 0)
|
|
135
|
+
return { allowed: true };
|
|
136
|
+
const cmd = command.trim();
|
|
137
|
+
for (const prefix of allowed) {
|
|
138
|
+
if (cmd === prefix || cmd.startsWith(prefix + ' ')) {
|
|
139
|
+
return { allowed: true };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Extract first word for the error message
|
|
143
|
+
const firstWord = cmd.split(/\s+/)[0] || cmd.substring(0, 30);
|
|
144
|
+
return {
|
|
145
|
+
allowed: false,
|
|
146
|
+
reason: `Tool "${toolName}" cannot run "${firstWord}" (allowed commands: ${allowed.join(', ')})`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Check if output size is within the cap. */
|
|
150
|
+
checkOutputSize(maxKb, actualKb, toolName) {
|
|
151
|
+
if (maxKb === undefined || maxKb <= 0)
|
|
152
|
+
return { allowed: true };
|
|
153
|
+
if (actualKb <= maxKb)
|
|
154
|
+
return { allowed: true };
|
|
155
|
+
return {
|
|
156
|
+
allowed: false,
|
|
157
|
+
reason: `Tool "${toolName}" output ${actualKb}KB exceeds cap of ${maxKb}KB`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** Simple glob matching (** = any depth, * = one segment). */
|
|
161
|
+
matchGlob(relativePath, pattern) {
|
|
162
|
+
const cleanPattern = pattern.replace(/^\.\//, '');
|
|
163
|
+
const cleanPath = relativePath.replace(/^\.\//, '');
|
|
164
|
+
// Exact match
|
|
165
|
+
if (cleanPath === cleanPattern)
|
|
166
|
+
return true;
|
|
167
|
+
// Prefix match (directory)
|
|
168
|
+
if (cleanPath.startsWith(cleanPattern + '/'))
|
|
169
|
+
return true;
|
|
170
|
+
if (cleanPath.startsWith(cleanPattern + path.sep))
|
|
171
|
+
return true;
|
|
172
|
+
// Glob expansion
|
|
173
|
+
if (pattern.includes('*')) {
|
|
174
|
+
const regex = new RegExp('^' +
|
|
175
|
+
cleanPattern
|
|
176
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
177
|
+
.replace(/\*\*/g, '<<GLOBSTAR>>')
|
|
178
|
+
.replace(/\*/g, '[^/]*')
|
|
179
|
+
.replace(/<<GLOBSTAR>>/g, '.*') +
|
|
180
|
+
'$');
|
|
181
|
+
return regex.test(cleanPath);
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.CapabilityChecker = CapabilityChecker;
|
|
187
|
+
//# sourceMappingURL=capabilities.js.map
|