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.
- package/dist/agent.d.ts +13 -0
- package/dist/agent.js +63 -4
- package/dist/audit.d.ts +61 -0
- package/dist/audit.js +231 -0
- package/dist/cli.js +181 -26
- package/dist/mcp.js +54 -3
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +71 -7
- package/dist/plugins.d.ts +0 -14
- package/dist/plugins.js +27 -14
- 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/secrets.d.ts +26 -0
- package/dist/secrets.js +86 -0
- package/dist/security.d.ts +18 -0
- package/dist/security.js +167 -0
- package/dist/telemetry.d.ts +73 -0
- package/dist/telemetry.js +286 -0
- package/dist/tools/batch-edit.js +20 -1
- package/dist/tools/edit.js +29 -5
- package/dist/tools/execute.js +81 -4
- package/dist/tools/package-manager.js +36 -0
- package/dist/tools/web-fetch.js +42 -2
- package/dist/tools/write.js +17 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Docker Sandbox Execution for CodeBot v1.7.0
|
|
4
|
+
*
|
|
5
|
+
* Runs shell commands inside disposable Docker containers for isolation.
|
|
6
|
+
* Features:
|
|
7
|
+
* - Read-only root filesystem
|
|
8
|
+
* - Project directory mounted read-write
|
|
9
|
+
* - No network by default (configurable)
|
|
10
|
+
* - CPU, memory, PID limits
|
|
11
|
+
* - Automatic container cleanup
|
|
12
|
+
* - Graceful fallback to host execution when Docker unavailable
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.isDockerAvailable = isDockerAvailable;
|
|
49
|
+
exports.resetDockerCheck = resetDockerCheck;
|
|
50
|
+
exports.sandboxExec = sandboxExec;
|
|
51
|
+
exports.ensureSandboxImage = ensureSandboxImage;
|
|
52
|
+
exports.getSandboxInfo = getSandboxInfo;
|
|
53
|
+
const child_process_1 = require("child_process");
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
// ── Docker Detection ──
|
|
56
|
+
let _dockerAvailable = null;
|
|
57
|
+
/** Check if Docker is installed and the daemon is running */
|
|
58
|
+
function isDockerAvailable() {
|
|
59
|
+
if (_dockerAvailable !== null)
|
|
60
|
+
return _dockerAvailable;
|
|
61
|
+
try {
|
|
62
|
+
(0, child_process_1.execSync)('docker info', {
|
|
63
|
+
timeout: 5000,
|
|
64
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
65
|
+
encoding: 'utf-8',
|
|
66
|
+
});
|
|
67
|
+
_dockerAvailable = true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
_dockerAvailable = false;
|
|
71
|
+
}
|
|
72
|
+
return _dockerAvailable;
|
|
73
|
+
}
|
|
74
|
+
/** Reset the cached Docker availability check (for testing) */
|
|
75
|
+
function resetDockerCheck() {
|
|
76
|
+
_dockerAvailable = null;
|
|
77
|
+
}
|
|
78
|
+
// ── Sandbox Execution ──
|
|
79
|
+
const DEFAULT_CONFIG = {
|
|
80
|
+
cpus: 2,
|
|
81
|
+
memoryMb: 512,
|
|
82
|
+
pidsLimit: 100,
|
|
83
|
+
network: false,
|
|
84
|
+
timeoutMs: 120_000,
|
|
85
|
+
workDir: '/workspace',
|
|
86
|
+
image: 'node:20-slim',
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Execute a command inside a Docker sandbox.
|
|
90
|
+
*
|
|
91
|
+
* The project directory is mounted at /workspace (read-write).
|
|
92
|
+
* Root filesystem is read-only. /tmp is tmpfs (100MB).
|
|
93
|
+
* Network is disabled by default.
|
|
94
|
+
*
|
|
95
|
+
* Falls back to host execution if Docker is unavailable.
|
|
96
|
+
*/
|
|
97
|
+
function sandboxExec(command, projectDir, config) {
|
|
98
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
99
|
+
const resolvedProjectDir = path.resolve(projectDir);
|
|
100
|
+
if (!isDockerAvailable()) {
|
|
101
|
+
return hostFallback(command, resolvedProjectDir, cfg.timeoutMs);
|
|
102
|
+
}
|
|
103
|
+
// Build docker run command
|
|
104
|
+
const dockerArgs = [
|
|
105
|
+
'docker', 'run',
|
|
106
|
+
'--rm', // Cleanup on exit
|
|
107
|
+
'--read-only', // Read-only root filesystem
|
|
108
|
+
'--tmpfs', '/tmp:size=100m', // Writable /tmp
|
|
109
|
+
`--cpus="${cfg.cpus}"`, // CPU limit
|
|
110
|
+
`--memory="${cfg.memoryMb}m"`, // Memory limit
|
|
111
|
+
`--pids-limit`, String(cfg.pidsLimit), // PID limit
|
|
112
|
+
'--security-opt', 'no-new-privileges', // No privilege escalation
|
|
113
|
+
'--cap-drop=ALL', // Drop all capabilities
|
|
114
|
+
];
|
|
115
|
+
// Network
|
|
116
|
+
if (!cfg.network) {
|
|
117
|
+
dockerArgs.push('--network', 'none');
|
|
118
|
+
}
|
|
119
|
+
// Mount project directory
|
|
120
|
+
dockerArgs.push('-v', `${resolvedProjectDir}:${cfg.workDir}:rw`);
|
|
121
|
+
// Working directory
|
|
122
|
+
dockerArgs.push('-w', cfg.workDir);
|
|
123
|
+
// Image
|
|
124
|
+
dockerArgs.push(cfg.image);
|
|
125
|
+
// Command (via sh -c for shell features)
|
|
126
|
+
dockerArgs.push('sh', '-c', command);
|
|
127
|
+
const dockerCmd = dockerArgs.join(' ');
|
|
128
|
+
try {
|
|
129
|
+
const stdout = (0, child_process_1.execSync)(dockerCmd, {
|
|
130
|
+
timeout: cfg.timeoutMs,
|
|
131
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
132
|
+
encoding: 'utf-8',
|
|
133
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
134
|
+
});
|
|
135
|
+
return { stdout: stdout || '', stderr: '', exitCode: 0, sandboxed: true };
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const e = err;
|
|
139
|
+
return {
|
|
140
|
+
stdout: e.stdout || '',
|
|
141
|
+
stderr: e.stderr || '',
|
|
142
|
+
exitCode: e.status || 1,
|
|
143
|
+
sandboxed: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fallback: execute on host with existing security measures.
|
|
149
|
+
* Used when Docker is not available.
|
|
150
|
+
*/
|
|
151
|
+
function hostFallback(command, cwd, timeoutMs) {
|
|
152
|
+
try {
|
|
153
|
+
const stdout = (0, child_process_1.execSync)(command, {
|
|
154
|
+
cwd,
|
|
155
|
+
timeout: timeoutMs,
|
|
156
|
+
maxBuffer: 1024 * 1024,
|
|
157
|
+
encoding: 'utf-8',
|
|
158
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
159
|
+
});
|
|
160
|
+
return { stdout: stdout || '', stderr: '', exitCode: 0, sandboxed: false };
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const e = err;
|
|
164
|
+
return {
|
|
165
|
+
stdout: e.stdout || '',
|
|
166
|
+
stderr: e.stderr || '',
|
|
167
|
+
exitCode: e.status || 1,
|
|
168
|
+
sandboxed: false,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build or pull the sandbox Docker image.
|
|
174
|
+
* Call this during `codebot --setup` or first sandbox use.
|
|
175
|
+
*/
|
|
176
|
+
function ensureSandboxImage(image) {
|
|
177
|
+
const img = image || DEFAULT_CONFIG.image;
|
|
178
|
+
if (!isDockerAvailable()) {
|
|
179
|
+
return { ready: false, error: 'Docker is not available' };
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
// Check if image exists locally
|
|
183
|
+
(0, child_process_1.execSync)(`docker image inspect ${img}`, {
|
|
184
|
+
timeout: 10_000,
|
|
185
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
186
|
+
});
|
|
187
|
+
return { ready: true };
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Image not found, try to pull
|
|
191
|
+
try {
|
|
192
|
+
(0, child_process_1.execSync)(`docker pull ${img}`, {
|
|
193
|
+
timeout: 120_000,
|
|
194
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
195
|
+
});
|
|
196
|
+
return { ready: true };
|
|
197
|
+
}
|
|
198
|
+
catch (pullErr) {
|
|
199
|
+
const msg = pullErr instanceof Error ? pullErr.message : String(pullErr);
|
|
200
|
+
return { ready: false, error: `Failed to pull ${img}: ${msg}` };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get a summary of sandbox configuration for display.
|
|
206
|
+
*/
|
|
207
|
+
function getSandboxInfo() {
|
|
208
|
+
return {
|
|
209
|
+
available: isDockerAvailable(),
|
|
210
|
+
image: DEFAULT_CONFIG.image,
|
|
211
|
+
defaults: { ...DEFAULT_CONFIG },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=sandbox.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret detection module for CodeBot.
|
|
3
|
+
*
|
|
4
|
+
* Scans content for common credential patterns (API keys, tokens, passwords).
|
|
5
|
+
* Returns matches with line numbers and masked excerpts.
|
|
6
|
+
* Used to warn before writing secrets to files — does NOT block writes.
|
|
7
|
+
*/
|
|
8
|
+
export interface SecretMatch {
|
|
9
|
+
type: string;
|
|
10
|
+
line: number;
|
|
11
|
+
snippet: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Scan content for secrets. Returns array of matches with line numbers and masked snippets.
|
|
15
|
+
*/
|
|
16
|
+
export declare function scanForSecrets(content: string): SecretMatch[];
|
|
17
|
+
/**
|
|
18
|
+
* Quick check: does the content contain any secrets?
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasSecrets(content: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Mask secrets in an arbitrary string (e.g., for audit logging).
|
|
23
|
+
* Replaces all detected secret matches with masked versions.
|
|
24
|
+
*/
|
|
25
|
+
export declare function maskSecretsInString(text: string): string;
|
|
26
|
+
//# sourceMappingURL=secrets.d.ts.map
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Secret detection module for CodeBot.
|
|
4
|
+
*
|
|
5
|
+
* Scans content for common credential patterns (API keys, tokens, passwords).
|
|
6
|
+
* Returns matches with line numbers and masked excerpts.
|
|
7
|
+
* Used to warn before writing secrets to files — does NOT block writes.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.scanForSecrets = scanForSecrets;
|
|
11
|
+
exports.hasSecrets = hasSecrets;
|
|
12
|
+
exports.maskSecretsInString = maskSecretsInString;
|
|
13
|
+
const SECRET_PATTERNS = [
|
|
14
|
+
{ name: 'aws_access_key', pattern: /AKIA[0-9A-Z]{16}/ },
|
|
15
|
+
{ name: 'aws_secret_key', pattern: /(?:aws_secret_access_key|aws_secret)\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}/ },
|
|
16
|
+
{ name: 'private_key', pattern: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/ },
|
|
17
|
+
{ name: 'github_token', pattern: /gh[ps]_[A-Za-z0-9_]{36,}/ },
|
|
18
|
+
{ name: 'github_oauth', pattern: /gho_[A-Za-z0-9_]{36,}/ },
|
|
19
|
+
{ name: 'generic_api_key', pattern: /(?:api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{20,}/i },
|
|
20
|
+
{ name: 'jwt', pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_\-]+/ },
|
|
21
|
+
{ name: 'password_assign', pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/i },
|
|
22
|
+
{ name: 'connection_string', pattern: /(?:mongodb|postgres|postgresql|mysql|redis|amqp):\/\/[^\s]{10,}/ },
|
|
23
|
+
{ name: 'slack_token', pattern: /xox[bprs]-[0-9]{10,}-[A-Za-z0-9\-]+/ },
|
|
24
|
+
{ name: 'slack_webhook', pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/ },
|
|
25
|
+
{ name: 'generic_secret', pattern: /(?:secret|token|credential|auth_token)\s*[:=]\s*['"][^'"]{16,}['"]/i },
|
|
26
|
+
{ name: 'npm_token', pattern: /\/\/registry\.npmjs\.org\/:_authToken=[^\s]+/ },
|
|
27
|
+
{ name: 'sendgrid_key', pattern: /SG\.[A-Za-z0-9_\-]{22}\.[A-Za-z0-9_\-]{43}/ },
|
|
28
|
+
{ name: 'stripe_key', pattern: /sk_(live|test)_[A-Za-z0-9]{24,}/ },
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Mask a matched secret for safe display.
|
|
32
|
+
* Shows first 4 chars + **** + last 4 chars for strings >= 12 chars.
|
|
33
|
+
* For shorter strings, shows first 2 + **** + last 2.
|
|
34
|
+
*/
|
|
35
|
+
function maskSecret(match) {
|
|
36
|
+
if (match.length >= 12) {
|
|
37
|
+
return match.substring(0, 4) + '****' + match.substring(match.length - 4);
|
|
38
|
+
}
|
|
39
|
+
if (match.length >= 6) {
|
|
40
|
+
return match.substring(0, 2) + '****' + match.substring(match.length - 2);
|
|
41
|
+
}
|
|
42
|
+
return '****';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Scan content for secrets. Returns array of matches with line numbers and masked snippets.
|
|
46
|
+
*/
|
|
47
|
+
function scanForSecrets(content) {
|
|
48
|
+
const matches = [];
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
const line = lines[i];
|
|
52
|
+
for (const { name, pattern } of SECRET_PATTERNS) {
|
|
53
|
+
const match = line.match(pattern);
|
|
54
|
+
if (match) {
|
|
55
|
+
matches.push({
|
|
56
|
+
type: name,
|
|
57
|
+
line: i + 1,
|
|
58
|
+
snippet: maskSecret(match[0]),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return matches;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Quick check: does the content contain any secrets?
|
|
67
|
+
*/
|
|
68
|
+
function hasSecrets(content) {
|
|
69
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
70
|
+
if (pattern.test(content))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Mask secrets in an arbitrary string (e.g., for audit logging).
|
|
77
|
+
* Replaces all detected secret matches with masked versions.
|
|
78
|
+
*/
|
|
79
|
+
function maskSecretsInString(text) {
|
|
80
|
+
let masked = text;
|
|
81
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
82
|
+
masked = masked.replace(new RegExp(pattern.source, pattern.flags + (pattern.flags.includes('g') ? '' : 'g')), match => maskSecret(match));
|
|
83
|
+
}
|
|
84
|
+
return masked;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PathSafetyResult {
|
|
2
|
+
safe: boolean;
|
|
3
|
+
reason?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Check if a file path is safe for write/edit operations.
|
|
7
|
+
*
|
|
8
|
+
* Resolves symlinks, checks against blocked system paths,
|
|
9
|
+
* and verifies the path is within the project or user home.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isPathSafe(targetPath: string, projectRoot: string): PathSafetyResult;
|
|
12
|
+
/**
|
|
13
|
+
* Check if a working directory is safe for command execution.
|
|
14
|
+
*
|
|
15
|
+
* Ensures the CWD exists, is a directory, and is under the project root.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isCwdSafe(cwd: string, projectRoot: string): PathSafetyResult;
|
|
18
|
+
//# sourceMappingURL=security.d.ts.map
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isPathSafe = isPathSafe;
|
|
37
|
+
exports.isCwdSafe = isCwdSafe;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
/**
|
|
42
|
+
* Path safety module for CodeBot.
|
|
43
|
+
*
|
|
44
|
+
* Prevents tools from reading/writing system-critical files and directories.
|
|
45
|
+
* Resolves symlinks to prevent bypass attacks.
|
|
46
|
+
*/
|
|
47
|
+
/** System-critical absolute paths that should NEVER be written to */
|
|
48
|
+
const BLOCKED_ABSOLUTE_PATHS = [
|
|
49
|
+
'/etc', '/usr', '/bin', '/sbin', '/boot', '/dev', '/proc', '/sys',
|
|
50
|
+
'/var/log', '/var/run', '/lib', '/lib64',
|
|
51
|
+
// macOS system directories
|
|
52
|
+
'/System', '/Library',
|
|
53
|
+
// Windows system directories
|
|
54
|
+
'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)',
|
|
55
|
+
];
|
|
56
|
+
/** Home-relative sensitive directories/files that should NEVER be written to */
|
|
57
|
+
const BLOCKED_HOME_RELATIVE = [
|
|
58
|
+
'.ssh',
|
|
59
|
+
'.gnupg',
|
|
60
|
+
'.aws/credentials',
|
|
61
|
+
'.config/gcloud',
|
|
62
|
+
'.bashrc',
|
|
63
|
+
'.bash_profile',
|
|
64
|
+
'.zshrc',
|
|
65
|
+
'.profile',
|
|
66
|
+
'.gitconfig',
|
|
67
|
+
'.npmrc',
|
|
68
|
+
];
|
|
69
|
+
/**
|
|
70
|
+
* Check if a file path is safe for write/edit operations.
|
|
71
|
+
*
|
|
72
|
+
* Resolves symlinks, checks against blocked system paths,
|
|
73
|
+
* and verifies the path is within the project or user home.
|
|
74
|
+
*/
|
|
75
|
+
function isPathSafe(targetPath, projectRoot) {
|
|
76
|
+
try {
|
|
77
|
+
const resolved = path.resolve(targetPath);
|
|
78
|
+
// Resolve symlinks — for new files, resolve the parent directory
|
|
79
|
+
let realPath;
|
|
80
|
+
try {
|
|
81
|
+
realPath = fs.realpathSync(resolved);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// File doesn't exist yet — resolve the parent
|
|
85
|
+
const parentDir = path.dirname(resolved);
|
|
86
|
+
try {
|
|
87
|
+
const realParent = fs.realpathSync(parentDir);
|
|
88
|
+
realPath = path.join(realParent, path.basename(resolved));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Parent doesn't exist either — use the resolved path as-is
|
|
92
|
+
realPath = resolved;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Check against blocked absolute paths
|
|
96
|
+
const normalizedPath = realPath.replace(/\\/g, '/').toLowerCase();
|
|
97
|
+
for (const blocked of BLOCKED_ABSOLUTE_PATHS) {
|
|
98
|
+
const normalizedBlocked = blocked.replace(/\\/g, '/').toLowerCase();
|
|
99
|
+
if (normalizedPath === normalizedBlocked || normalizedPath.startsWith(normalizedBlocked + '/')) {
|
|
100
|
+
return { safe: false, reason: `Blocked: "${realPath}" is inside system directory "${blocked}"` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Check against home-relative sensitive paths
|
|
104
|
+
const home = os.homedir();
|
|
105
|
+
for (const relative of BLOCKED_HOME_RELATIVE) {
|
|
106
|
+
const blockedPath = path.join(home, relative);
|
|
107
|
+
const normalizedBlockedHome = blockedPath.replace(/\\/g, '/').toLowerCase();
|
|
108
|
+
if (normalizedPath === normalizedBlockedHome || normalizedPath.startsWith(normalizedBlockedHome + '/')) {
|
|
109
|
+
return { safe: false, reason: `Blocked: "${realPath}" is a sensitive file/directory (~/${relative})` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Verify path is under project root or user home
|
|
113
|
+
const normalizedProject = path.resolve(projectRoot).replace(/\\/g, '/').toLowerCase();
|
|
114
|
+
const normalizedHome = home.replace(/\\/g, '/').toLowerCase();
|
|
115
|
+
const isUnderProject = normalizedPath.startsWith(normalizedProject + '/') || normalizedPath === normalizedProject;
|
|
116
|
+
const isUnderHome = normalizedPath.startsWith(normalizedHome + '/') || normalizedPath === normalizedHome;
|
|
117
|
+
if (!isUnderProject && !isUnderHome) {
|
|
118
|
+
return { safe: false, reason: `Blocked: "${realPath}" is outside both project root and user home directory` };
|
|
119
|
+
}
|
|
120
|
+
return { safe: true };
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
return { safe: false, reason: `Path validation error: ${err instanceof Error ? err.message : String(err)}` };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if a working directory is safe for command execution.
|
|
128
|
+
*
|
|
129
|
+
* Ensures the CWD exists, is a directory, and is under the project root.
|
|
130
|
+
*/
|
|
131
|
+
function isCwdSafe(cwd, projectRoot) {
|
|
132
|
+
try {
|
|
133
|
+
const resolved = path.resolve(cwd);
|
|
134
|
+
// Check it exists and is a directory
|
|
135
|
+
try {
|
|
136
|
+
const stat = fs.statSync(resolved);
|
|
137
|
+
if (!stat.isDirectory()) {
|
|
138
|
+
return { safe: false, reason: `"${resolved}" is not a directory` };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { safe: false, reason: `Directory does not exist: "${resolved}"` };
|
|
143
|
+
}
|
|
144
|
+
// Resolve symlinks
|
|
145
|
+
let realPath;
|
|
146
|
+
try {
|
|
147
|
+
realPath = fs.realpathSync(resolved);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
realPath = resolved;
|
|
151
|
+
}
|
|
152
|
+
// Verify it's under project root or user home
|
|
153
|
+
const normalizedPath = realPath.replace(/\\/g, '/').toLowerCase();
|
|
154
|
+
const normalizedProject = path.resolve(projectRoot).replace(/\\/g, '/').toLowerCase();
|
|
155
|
+
const normalizedHome = os.homedir().replace(/\\/g, '/').toLowerCase();
|
|
156
|
+
const isUnderProject = normalizedPath.startsWith(normalizedProject + '/') || normalizedPath === normalizedProject;
|
|
157
|
+
const isUnderHome = normalizedPath.startsWith(normalizedHome + '/') || normalizedPath === normalizedHome;
|
|
158
|
+
if (!isUnderProject && !isUnderHome) {
|
|
159
|
+
return { safe: false, reason: `CWD "${realPath}" is outside project root and user home directory` };
|
|
160
|
+
}
|
|
161
|
+
return { safe: true };
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return { safe: false, reason: `CWD validation error: ${err instanceof Error ? err.message : String(err)}` };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token & Cost Tracking for CodeBot v1.7.0
|
|
3
|
+
*
|
|
4
|
+
* Tracks per-request and per-session token usage and estimated costs.
|
|
5
|
+
* Supports cost limits and historical usage queries.
|
|
6
|
+
*/
|
|
7
|
+
export interface UsageRecord {
|
|
8
|
+
timestamp: string;
|
|
9
|
+
model: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
inputTokens: number;
|
|
12
|
+
outputTokens: number;
|
|
13
|
+
costUsd: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SessionSummary {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
model: string;
|
|
18
|
+
provider: string;
|
|
19
|
+
startTime: string;
|
|
20
|
+
endTime: string;
|
|
21
|
+
totalInputTokens: number;
|
|
22
|
+
totalOutputTokens: number;
|
|
23
|
+
totalCostUsd: number;
|
|
24
|
+
requestCount: number;
|
|
25
|
+
toolCalls: number;
|
|
26
|
+
filesModified: number;
|
|
27
|
+
}
|
|
28
|
+
export declare class TokenTracker {
|
|
29
|
+
private model;
|
|
30
|
+
private provider;
|
|
31
|
+
private sessionId;
|
|
32
|
+
private records;
|
|
33
|
+
private toolCallCount;
|
|
34
|
+
private filesModifiedSet;
|
|
35
|
+
private startTime;
|
|
36
|
+
private costLimitUsd;
|
|
37
|
+
constructor(model: string, provider: string, sessionId?: string);
|
|
38
|
+
/** Set cost limit in USD. 0 = no limit. */
|
|
39
|
+
setCostLimit(usd: number): void;
|
|
40
|
+
/** Record token usage from an LLM request */
|
|
41
|
+
recordUsage(inputTokens: number, outputTokens: number): UsageRecord;
|
|
42
|
+
/** Record a tool call (for summary) */
|
|
43
|
+
recordToolCall(): void;
|
|
44
|
+
/** Record a file modification (for summary) */
|
|
45
|
+
recordFileModified(filePath: string): void;
|
|
46
|
+
/** Check if cost limit has been exceeded */
|
|
47
|
+
isOverBudget(): boolean;
|
|
48
|
+
/** Get remaining budget in USD (Infinity if no limit) */
|
|
49
|
+
getRemainingBudget(): number;
|
|
50
|
+
getTotalInputTokens(): number;
|
|
51
|
+
getTotalOutputTokens(): number;
|
|
52
|
+
getTotalCost(): number;
|
|
53
|
+
getRequestCount(): number;
|
|
54
|
+
/** Generate a session summary */
|
|
55
|
+
getSummary(): SessionSummary;
|
|
56
|
+
/** Format cost for display */
|
|
57
|
+
formatCost(): string;
|
|
58
|
+
/** Format a compact status line for CLI */
|
|
59
|
+
formatStatusLine(): string;
|
|
60
|
+
/** Save session usage to ~/.codebot/usage/ for historical tracking */
|
|
61
|
+
saveUsage(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Load historical usage from ~/.codebot/usage/
|
|
64
|
+
*/
|
|
65
|
+
static loadHistory(days?: number): SessionSummary[];
|
|
66
|
+
/**
|
|
67
|
+
* Format a historical usage report.
|
|
68
|
+
*/
|
|
69
|
+
static formatUsageReport(days?: number): string;
|
|
70
|
+
private getPricing;
|
|
71
|
+
private isLocalModel;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=telemetry.d.ts.map
|