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.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Policy Engine for CodeBot v1.7.0
3
+ *
4
+ * Loads, validates, and enforces declarative security policies.
5
+ * Policy files: .codebot/policy.json (project) + ~/.codebot/policy.json (global)
6
+ * Project policy overrides global policy where specified.
7
+ */
8
+ export interface PolicyExecution {
9
+ sandbox?: 'docker' | 'host' | 'auto';
10
+ network?: boolean;
11
+ timeout_seconds?: number;
12
+ max_memory_mb?: number;
13
+ }
14
+ export interface PolicyFilesystem {
15
+ writable_paths?: string[];
16
+ read_only_paths?: string[];
17
+ denied_paths?: string[];
18
+ allow_outside_project?: boolean;
19
+ }
20
+ export interface PolicyToolPermission {
21
+ [toolName: string]: 'auto' | 'prompt' | 'always-ask';
22
+ }
23
+ export interface PolicyTools {
24
+ enabled?: string[];
25
+ disabled?: string[];
26
+ permissions?: PolicyToolPermission;
27
+ }
28
+ export interface PolicySecrets {
29
+ block_on_detect?: boolean;
30
+ scan_on_write?: boolean;
31
+ allowed_patterns?: string[];
32
+ }
33
+ export interface PolicyGit {
34
+ always_branch?: boolean;
35
+ branch_prefix?: string;
36
+ require_tests_before_commit?: boolean;
37
+ never_push_main?: boolean;
38
+ }
39
+ export interface PolicyMcp {
40
+ allowed_servers?: string[];
41
+ blocked_servers?: string[];
42
+ }
43
+ export interface PolicyLimits {
44
+ max_iterations?: number;
45
+ max_file_size_kb?: number;
46
+ max_files_per_operation?: number;
47
+ cost_limit_usd?: number;
48
+ }
49
+ export interface Policy {
50
+ version?: string;
51
+ execution?: PolicyExecution;
52
+ filesystem?: PolicyFilesystem;
53
+ tools?: PolicyTools;
54
+ secrets?: PolicySecrets;
55
+ git?: PolicyGit;
56
+ mcp?: PolicyMcp;
57
+ limits?: PolicyLimits;
58
+ }
59
+ export declare const DEFAULT_POLICY: Required<Policy>;
60
+ /**
61
+ * Load and merge policies from project + global locations.
62
+ * Project policy overrides global where specified.
63
+ */
64
+ export declare function loadPolicy(projectRoot?: string): Policy;
65
+ export declare class PolicyEnforcer {
66
+ private policy;
67
+ private projectRoot;
68
+ constructor(policy?: Policy, projectRoot?: string);
69
+ getPolicy(): Policy;
70
+ /** Check if a tool is enabled by policy. Returns { allowed, reason }. */
71
+ isToolAllowed(toolName: string): {
72
+ allowed: boolean;
73
+ reason?: string;
74
+ };
75
+ /** Get the permission level for a tool (policy override or null for default). */
76
+ getToolPermission(toolName: string): 'auto' | 'prompt' | 'always-ask' | null;
77
+ /** Check if a path is writable according to policy. */
78
+ isPathWritable(filePath: string): {
79
+ allowed: boolean;
80
+ reason?: string;
81
+ };
82
+ /** Get sandbox mode. */
83
+ getSandboxMode(): 'docker' | 'host' | 'auto';
84
+ /** Check if network is allowed for executed commands. */
85
+ isNetworkAllowed(): boolean;
86
+ /** Get execution timeout in milliseconds. */
87
+ getTimeoutMs(): number;
88
+ /** Get max memory in MB for sandbox. */
89
+ getMaxMemoryMb(): number;
90
+ /** Check if agent should always work on a branch. */
91
+ shouldAlwaysBranch(): boolean;
92
+ /** Get branch prefix for auto-created branches. */
93
+ getBranchPrefix(): string;
94
+ /** Check if pushing to main/master is blocked. */
95
+ isMainPushBlocked(): boolean;
96
+ /** Should secrets block writes (vs just warn)? */
97
+ shouldBlockSecrets(): boolean;
98
+ /** Should scan for secrets on write? */
99
+ shouldScanSecrets(): boolean;
100
+ /** Check if an MCP server is allowed. */
101
+ isMcpServerAllowed(serverName: string): {
102
+ allowed: boolean;
103
+ reason?: string;
104
+ };
105
+ /** Get max iterations for the agent loop. */
106
+ getMaxIterations(): number;
107
+ /** Get max file size in bytes for write operations. */
108
+ getMaxFileSizeBytes(): number;
109
+ /** Get cost limit in USD (0 = no limit). */
110
+ getCostLimitUsd(): number;
111
+ /**
112
+ * Simple glob-like pattern matching:
113
+ * - `*` matches any single path component
114
+ * - `**` matches any number of path components
115
+ * - `.env` matches exact filename
116
+ */
117
+ private matchesPattern;
118
+ }
119
+ /**
120
+ * Generate a default policy file content for `codebot --init-policy`.
121
+ */
122
+ export declare function generateDefaultPolicyFile(): string;
123
+ //# sourceMappingURL=policy.d.ts.map
package/dist/policy.js ADDED
@@ -0,0 +1,418 @@
1
+ "use strict";
2
+ /**
3
+ * Policy Engine for CodeBot v1.7.0
4
+ *
5
+ * Loads, validates, and enforces declarative security policies.
6
+ * Policy files: .codebot/policy.json (project) + ~/.codebot/policy.json (global)
7
+ * Project policy overrides global policy where specified.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.PolicyEnforcer = exports.DEFAULT_POLICY = void 0;
44
+ exports.loadPolicy = loadPolicy;
45
+ exports.generateDefaultPolicyFile = generateDefaultPolicyFile;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const os = __importStar(require("os"));
49
+ // ── Default Policy ──
50
+ exports.DEFAULT_POLICY = {
51
+ version: '1.0',
52
+ execution: {
53
+ sandbox: 'auto',
54
+ network: true,
55
+ timeout_seconds: 120,
56
+ max_memory_mb: 512,
57
+ },
58
+ filesystem: {
59
+ writable_paths: [], // empty = all project paths allowed
60
+ read_only_paths: [],
61
+ denied_paths: ['.env', '.env.local', '.env.production'],
62
+ allow_outside_project: false,
63
+ },
64
+ tools: {
65
+ enabled: [], // empty = all tools enabled
66
+ disabled: [],
67
+ permissions: {},
68
+ },
69
+ secrets: {
70
+ block_on_detect: false,
71
+ scan_on_write: true,
72
+ allowed_patterns: [],
73
+ },
74
+ git: {
75
+ always_branch: false,
76
+ branch_prefix: 'codebot/',
77
+ require_tests_before_commit: false,
78
+ never_push_main: true,
79
+ },
80
+ mcp: {
81
+ allowed_servers: [], // empty = all allowed
82
+ blocked_servers: [],
83
+ },
84
+ limits: {
85
+ max_iterations: 50,
86
+ max_file_size_kb: 500,
87
+ max_files_per_operation: 20,
88
+ cost_limit_usd: 0, // 0 = no limit
89
+ },
90
+ };
91
+ // ── Policy Loader ──
92
+ /**
93
+ * Load and merge policies from project + global locations.
94
+ * Project policy overrides global where specified.
95
+ */
96
+ function loadPolicy(projectRoot) {
97
+ const root = projectRoot || process.cwd();
98
+ // Load global policy
99
+ const globalPath = path.join(os.homedir(), '.codebot', 'policy.json');
100
+ const globalPolicy = loadPolicyFile(globalPath);
101
+ // Load project policy
102
+ const projectPath = path.join(root, '.codebot', 'policy.json');
103
+ const projectPolicy = loadPolicyFile(projectPath);
104
+ // Merge: defaults ← global ← project (project wins)
105
+ return mergePolicies(exports.DEFAULT_POLICY, globalPolicy, projectPolicy);
106
+ }
107
+ function loadPolicyFile(filePath) {
108
+ try {
109
+ if (!fs.existsSync(filePath))
110
+ return null;
111
+ const content = fs.readFileSync(filePath, 'utf-8');
112
+ const parsed = JSON.parse(content);
113
+ if (!validatePolicy(parsed))
114
+ return null;
115
+ return parsed;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ /**
122
+ * Basic validation — ensures the policy file has a recognizable shape.
123
+ * Does NOT throw — returns false for invalid policies (fail-open with defaults).
124
+ */
125
+ function validatePolicy(obj) {
126
+ if (!obj || typeof obj !== 'object')
127
+ return false;
128
+ const p = obj;
129
+ // Version check
130
+ if (p.version !== undefined && typeof p.version !== 'string')
131
+ return false;
132
+ // Type check each section
133
+ if (p.execution !== undefined && typeof p.execution !== 'object')
134
+ return false;
135
+ if (p.filesystem !== undefined && typeof p.filesystem !== 'object')
136
+ return false;
137
+ if (p.tools !== undefined && typeof p.tools !== 'object')
138
+ return false;
139
+ if (p.secrets !== undefined && typeof p.secrets !== 'object')
140
+ return false;
141
+ if (p.git !== undefined && typeof p.git !== 'object')
142
+ return false;
143
+ if (p.mcp !== undefined && typeof p.mcp !== 'object')
144
+ return false;
145
+ if (p.limits !== undefined && typeof p.limits !== 'object')
146
+ return false;
147
+ return true;
148
+ }
149
+ /**
150
+ * Deep merge policies. Later arguments override earlier ones.
151
+ * Only defined keys in higher-priority policies override lower ones.
152
+ */
153
+ function mergePolicies(...policies) {
154
+ const result = {};
155
+ for (const policy of policies) {
156
+ if (!policy)
157
+ continue;
158
+ for (const [key, value] of Object.entries(policy)) {
159
+ if (value === undefined || value === null)
160
+ continue;
161
+ if (typeof value === 'object' && !Array.isArray(value)) {
162
+ // Deep merge objects
163
+ result[key] = { ...(result[key] || {}), ...value };
164
+ }
165
+ else {
166
+ result[key] = value;
167
+ }
168
+ }
169
+ }
170
+ return result;
171
+ }
172
+ // ── Policy Enforcer ──
173
+ class PolicyEnforcer {
174
+ policy;
175
+ projectRoot;
176
+ constructor(policy, projectRoot) {
177
+ this.policy = policy || loadPolicy(projectRoot);
178
+ this.projectRoot = projectRoot || process.cwd();
179
+ }
180
+ getPolicy() {
181
+ return this.policy;
182
+ }
183
+ // ── Tool Access ──
184
+ /** Check if a tool is enabled by policy. Returns { allowed, reason }. */
185
+ isToolAllowed(toolName) {
186
+ const tools = this.policy.tools;
187
+ if (!tools)
188
+ return { allowed: true };
189
+ // If explicit disabled list contains it, block
190
+ if (tools.disabled && tools.disabled.length > 0) {
191
+ if (tools.disabled.includes(toolName)) {
192
+ return { allowed: false, reason: `Tool "${toolName}" is disabled by policy` };
193
+ }
194
+ }
195
+ // If explicit enabled list exists and is non-empty, only those tools are allowed
196
+ if (tools.enabled && tools.enabled.length > 0) {
197
+ if (!tools.enabled.includes(toolName)) {
198
+ return { allowed: false, reason: `Tool "${toolName}" is not in the enabled tools list` };
199
+ }
200
+ }
201
+ return { allowed: true };
202
+ }
203
+ /** Get the permission level for a tool (policy override or null for default). */
204
+ getToolPermission(toolName) {
205
+ return this.policy.tools?.permissions?.[toolName] || null;
206
+ }
207
+ // ── Filesystem Access ──
208
+ /** Check if a path is writable according to policy. */
209
+ isPathWritable(filePath) {
210
+ const fs_policy = this.policy.filesystem;
211
+ if (!fs_policy)
212
+ return { allowed: true };
213
+ const resolved = path.resolve(filePath);
214
+ const relative = path.relative(this.projectRoot, resolved);
215
+ // Check denied paths first (highest priority)
216
+ if (fs_policy.denied_paths && fs_policy.denied_paths.length > 0) {
217
+ for (const denied of fs_policy.denied_paths) {
218
+ const deniedResolved = path.resolve(this.projectRoot, denied);
219
+ if (resolved === deniedResolved || resolved.startsWith(deniedResolved + path.sep)) {
220
+ return { allowed: false, reason: `Path "${relative}" is denied by policy` };
221
+ }
222
+ // Also check as a glob-like prefix
223
+ if (this.matchesPattern(relative, denied)) {
224
+ return { allowed: false, reason: `Path "${relative}" matches denied pattern "${denied}"` };
225
+ }
226
+ }
227
+ }
228
+ // Check read-only paths
229
+ if (fs_policy.read_only_paths && fs_policy.read_only_paths.length > 0) {
230
+ for (const ro of fs_policy.read_only_paths) {
231
+ const roResolved = path.resolve(this.projectRoot, ro);
232
+ if (resolved === roResolved || resolved.startsWith(roResolved + path.sep)) {
233
+ return { allowed: false, reason: `Path "${relative}" is read-only by policy` };
234
+ }
235
+ }
236
+ }
237
+ // Check writable paths (if specified, only these are writable)
238
+ if (fs_policy.writable_paths && fs_policy.writable_paths.length > 0) {
239
+ let matched = false;
240
+ for (const wp of fs_policy.writable_paths) {
241
+ const wpResolved = path.resolve(this.projectRoot, wp);
242
+ if (resolved === wpResolved || resolved.startsWith(wpResolved + path.sep)) {
243
+ matched = true;
244
+ break;
245
+ }
246
+ if (this.matchesPattern(relative, wp)) {
247
+ matched = true;
248
+ break;
249
+ }
250
+ }
251
+ if (!matched) {
252
+ return { allowed: false, reason: `Path "${relative}" is not in the writable paths list` };
253
+ }
254
+ }
255
+ return { allowed: true };
256
+ }
257
+ // ── Execution Policy ──
258
+ /** Get sandbox mode. */
259
+ getSandboxMode() {
260
+ return this.policy.execution?.sandbox || 'auto';
261
+ }
262
+ /** Check if network is allowed for executed commands. */
263
+ isNetworkAllowed() {
264
+ return this.policy.execution?.network !== false;
265
+ }
266
+ /** Get execution timeout in milliseconds. */
267
+ getTimeoutMs() {
268
+ const seconds = this.policy.execution?.timeout_seconds || 120;
269
+ return seconds * 1000;
270
+ }
271
+ /** Get max memory in MB for sandbox. */
272
+ getMaxMemoryMb() {
273
+ return this.policy.execution?.max_memory_mb || 512;
274
+ }
275
+ // ── Git Policy ──
276
+ /** Check if agent should always work on a branch. */
277
+ shouldAlwaysBranch() {
278
+ return this.policy.git?.always_branch === true;
279
+ }
280
+ /** Get branch prefix for auto-created branches. */
281
+ getBranchPrefix() {
282
+ return this.policy.git?.branch_prefix || 'codebot/';
283
+ }
284
+ /** Check if pushing to main/master is blocked. */
285
+ isMainPushBlocked() {
286
+ return this.policy.git?.never_push_main !== false; // default true
287
+ }
288
+ // ── Secrets Policy ──
289
+ /** Should secrets block writes (vs just warn)? */
290
+ shouldBlockSecrets() {
291
+ return this.policy.secrets?.block_on_detect === true;
292
+ }
293
+ /** Should scan for secrets on write? */
294
+ shouldScanSecrets() {
295
+ return this.policy.secrets?.scan_on_write !== false; // default true
296
+ }
297
+ // ── MCP Policy ──
298
+ /** Check if an MCP server is allowed. */
299
+ isMcpServerAllowed(serverName) {
300
+ const mcp = this.policy.mcp;
301
+ if (!mcp)
302
+ return { allowed: true };
303
+ // Blocked list takes priority
304
+ if (mcp.blocked_servers && mcp.blocked_servers.length > 0) {
305
+ if (mcp.blocked_servers.includes(serverName)) {
306
+ return { allowed: false, reason: `MCP server "${serverName}" is blocked by policy` };
307
+ }
308
+ }
309
+ // If allowed list is non-empty, only those servers are allowed
310
+ if (mcp.allowed_servers && mcp.allowed_servers.length > 0) {
311
+ if (!mcp.allowed_servers.includes(serverName)) {
312
+ return { allowed: false, reason: `MCP server "${serverName}" is not in the allowed list` };
313
+ }
314
+ }
315
+ return { allowed: true };
316
+ }
317
+ // ── Limits ──
318
+ /** Get max iterations for the agent loop. */
319
+ getMaxIterations() {
320
+ return this.policy.limits?.max_iterations || 50;
321
+ }
322
+ /** Get max file size in bytes for write operations. */
323
+ getMaxFileSizeBytes() {
324
+ return (this.policy.limits?.max_file_size_kb || 500) * 1024;
325
+ }
326
+ /** Get cost limit in USD (0 = no limit). */
327
+ getCostLimitUsd() {
328
+ return this.policy.limits?.cost_limit_usd || 0;
329
+ }
330
+ // ── Helpers ──
331
+ /**
332
+ * Simple glob-like pattern matching:
333
+ * - `*` matches any single path component
334
+ * - `**` matches any number of path components
335
+ * - `.env` matches exact filename
336
+ */
337
+ matchesPattern(relativePath, pattern) {
338
+ // Exact match
339
+ if (relativePath === pattern)
340
+ return true;
341
+ // Simple prefix match (handles ./src, ./tests)
342
+ const cleanPattern = pattern.replace(/^\.\//, '');
343
+ const cleanPath = relativePath.replace(/^\.\//, '');
344
+ if (cleanPath === cleanPattern)
345
+ return true;
346
+ if (cleanPath.startsWith(cleanPattern + '/'))
347
+ return true;
348
+ if (cleanPath.startsWith(cleanPattern + path.sep))
349
+ return true;
350
+ // Basename match (handles .env, .env.local)
351
+ if (!pattern.includes('/') && !pattern.includes(path.sep)) {
352
+ if (path.basename(relativePath) === pattern)
353
+ return true;
354
+ }
355
+ // Glob-like: ** matches any depth
356
+ if (pattern.includes('**')) {
357
+ const regex = new RegExp('^' +
358
+ pattern
359
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
360
+ .replace(/\*\*/g, '.*')
361
+ .replace(/\*/g, '[^/]*') +
362
+ '$');
363
+ return regex.test(cleanPath);
364
+ }
365
+ return false;
366
+ }
367
+ }
368
+ exports.PolicyEnforcer = PolicyEnforcer;
369
+ /**
370
+ * Generate a default policy file content for `codebot --init-policy`.
371
+ */
372
+ function generateDefaultPolicyFile() {
373
+ return JSON.stringify({
374
+ version: '1.0',
375
+ execution: {
376
+ sandbox: 'auto',
377
+ network: true,
378
+ timeout_seconds: 120,
379
+ max_memory_mb: 512,
380
+ },
381
+ filesystem: {
382
+ writable_paths: [],
383
+ read_only_paths: [],
384
+ denied_paths: ['.env', '.env.local', '.env.production'],
385
+ allow_outside_project: false,
386
+ },
387
+ tools: {
388
+ enabled: [],
389
+ disabled: [],
390
+ permissions: {
391
+ execute: 'always-ask',
392
+ write_file: 'prompt',
393
+ edit_file: 'prompt',
394
+ },
395
+ },
396
+ secrets: {
397
+ block_on_detect: false,
398
+ scan_on_write: true,
399
+ },
400
+ git: {
401
+ always_branch: false,
402
+ branch_prefix: 'codebot/',
403
+ require_tests_before_commit: false,
404
+ never_push_main: true,
405
+ },
406
+ mcp: {
407
+ allowed_servers: [],
408
+ blocked_servers: [],
409
+ },
410
+ limits: {
411
+ max_iterations: 50,
412
+ max_file_size_kb: 500,
413
+ max_files_per_operation: 20,
414
+ cost_limit_usd: 0,
415
+ },
416
+ }, null, 2) + '\n';
417
+ }
418
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Docker Sandbox Execution for CodeBot v1.7.0
3
+ *
4
+ * Runs shell commands inside disposable Docker containers for isolation.
5
+ * Features:
6
+ * - Read-only root filesystem
7
+ * - Project directory mounted read-write
8
+ * - No network by default (configurable)
9
+ * - CPU, memory, PID limits
10
+ * - Automatic container cleanup
11
+ * - Graceful fallback to host execution when Docker unavailable
12
+ */
13
+ export interface SandboxConfig {
14
+ /** Max CPU cores (default: 2) */
15
+ cpus?: number;
16
+ /** Max memory in MB (default: 512) */
17
+ memoryMb?: number;
18
+ /** Max PIDs in container (default: 100) */
19
+ pidsLimit?: number;
20
+ /** Allow network access (default: false) */
21
+ network?: boolean;
22
+ /** Timeout in ms (default: 120000) */
23
+ timeoutMs?: number;
24
+ /** Working directory inside container (default: /workspace) */
25
+ workDir?: string;
26
+ /** Docker image to use (default: node:20-slim) */
27
+ image?: string;
28
+ }
29
+ export interface SandboxResult {
30
+ stdout: string;
31
+ stderr: string;
32
+ exitCode: number;
33
+ sandboxed: boolean;
34
+ }
35
+ /** Check if Docker is installed and the daemon is running */
36
+ export declare function isDockerAvailable(): boolean;
37
+ /** Reset the cached Docker availability check (for testing) */
38
+ export declare function resetDockerCheck(): void;
39
+ /**
40
+ * Execute a command inside a Docker sandbox.
41
+ *
42
+ * The project directory is mounted at /workspace (read-write).
43
+ * Root filesystem is read-only. /tmp is tmpfs (100MB).
44
+ * Network is disabled by default.
45
+ *
46
+ * Falls back to host execution if Docker is unavailable.
47
+ */
48
+ export declare function sandboxExec(command: string, projectDir: string, config?: SandboxConfig): SandboxResult;
49
+ /**
50
+ * Build or pull the sandbox Docker image.
51
+ * Call this during `codebot --setup` or first sandbox use.
52
+ */
53
+ export declare function ensureSandboxImage(image?: string): {
54
+ ready: boolean;
55
+ error?: string;
56
+ };
57
+ /**
58
+ * Get a summary of sandbox configuration for display.
59
+ */
60
+ export declare function getSandboxInfo(): {
61
+ available: boolean;
62
+ image: string;
63
+ defaults: SandboxConfig;
64
+ };
65
+ //# sourceMappingURL=sandbox.d.ts.map