cli4ai 1.0.3 → 1.0.4
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/package.json +1 -1
- package/src/cli.ts +10 -0
- package/src/commands/run.ts +19 -2
- package/src/core/config.ts +15 -0
- package/src/core/execute.ts +60 -0
- package/src/mcp/server.ts +8 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -94,6 +94,8 @@ export function createProgram(): Command {
|
|
|
94
94
|
.command('run <package> [command] [args...]')
|
|
95
95
|
.description('Run a tool command')
|
|
96
96
|
.option('-e, --env <vars...>', 'Environment variables (KEY=value)')
|
|
97
|
+
.option('--scope <level>', 'Permission scope: read, write, or full (default: full)')
|
|
98
|
+
.option('--sandbox', 'Run in sandboxed environment with restricted file system access')
|
|
97
99
|
// Allow passing tool flags through (e.g. `cli4ai run chrome screenshot --full-page`)
|
|
98
100
|
.allowUnknownOption(true)
|
|
99
101
|
.addHelpText('after', `
|
|
@@ -101,10 +103,18 @@ export function createProgram(): Command {
|
|
|
101
103
|
Examples:
|
|
102
104
|
cli4ai run github trending
|
|
103
105
|
cli4ai run chrome screenshot https://example.com --full-page
|
|
106
|
+
cli4ai run github list-issues --scope read
|
|
107
|
+
cli4ai run untrusted-pkg process --sandbox
|
|
104
108
|
|
|
105
109
|
Pass-through:
|
|
106
110
|
Use "--" to pass flags that would otherwise be parsed by cli4ai:
|
|
107
111
|
cli4ai run <pkg> <cmd> -- --help
|
|
112
|
+
|
|
113
|
+
Security:
|
|
114
|
+
--scope read Only allow read operations (no mutations)
|
|
115
|
+
--scope write Allow write operations
|
|
116
|
+
--scope full Full access (default)
|
|
117
|
+
--sandbox Restrict file system access to temp directories
|
|
108
118
|
`)
|
|
109
119
|
.action(withErrorHandling(runCommand));
|
|
110
120
|
|
package/src/commands/run.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { outputError } from '../lib/cli.js';
|
|
6
|
-
import { executeTool, ExecuteToolError } from '../core/execute.js';
|
|
6
|
+
import { executeTool, ExecuteToolError, type ScopeLevel } from '../core/execute.js';
|
|
7
7
|
|
|
8
8
|
interface RunOptions {
|
|
9
9
|
env?: string[];
|
|
10
|
+
scope?: string;
|
|
11
|
+
sandbox?: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export async function runCommand(
|
|
@@ -26,6 +28,19 @@ export async function runCommand(
|
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
// Validate scope option
|
|
32
|
+
let scope: ScopeLevel = 'full';
|
|
33
|
+
if (options.scope) {
|
|
34
|
+
const validScopes: ScopeLevel[] = ['read', 'write', 'full'];
|
|
35
|
+
if (!validScopes.includes(options.scope as ScopeLevel)) {
|
|
36
|
+
outputError('INVALID_INPUT', `Invalid scope: ${options.scope}`, {
|
|
37
|
+
validScopes,
|
|
38
|
+
hint: 'Use --scope read, --scope write, or --scope full'
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
scope = options.scope as ScopeLevel;
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
try {
|
|
30
45
|
const result = await executeTool({
|
|
31
46
|
packageName,
|
|
@@ -33,7 +48,9 @@ export async function runCommand(
|
|
|
33
48
|
args,
|
|
34
49
|
cwd: process.cwd(),
|
|
35
50
|
env: extraEnv,
|
|
36
|
-
capture: 'inherit'
|
|
51
|
+
capture: 'inherit',
|
|
52
|
+
scope,
|
|
53
|
+
sandbox: options.sandbox ?? false
|
|
37
54
|
});
|
|
38
55
|
process.exitCode = result.exitCode;
|
|
39
56
|
return;
|
package/src/core/config.ts
CHANGED
|
@@ -104,6 +104,12 @@ export interface Config {
|
|
|
104
104
|
port: number;
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
+
// Audit logging configuration
|
|
108
|
+
audit: {
|
|
109
|
+
/** Enable audit logging for MCP tool calls */
|
|
110
|
+
enabled: boolean;
|
|
111
|
+
};
|
|
112
|
+
|
|
107
113
|
// Telemetry (future)
|
|
108
114
|
telemetry: boolean;
|
|
109
115
|
}
|
|
@@ -128,6 +134,9 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
128
134
|
transport: 'stdio',
|
|
129
135
|
port: 3100
|
|
130
136
|
},
|
|
137
|
+
audit: {
|
|
138
|
+
enabled: true
|
|
139
|
+
},
|
|
131
140
|
telemetry: false
|
|
132
141
|
};
|
|
133
142
|
|
|
@@ -197,6 +206,12 @@ function deepMerge(target: Config, source: Partial<Config>): Config {
|
|
|
197
206
|
port: source.mcp.port ?? target.mcp.port
|
|
198
207
|
};
|
|
199
208
|
}
|
|
209
|
+
// Deep merge audit config
|
|
210
|
+
if (source.audit !== undefined) {
|
|
211
|
+
result.audit = {
|
|
212
|
+
enabled: source.audit.enabled ?? target.audit.enabled
|
|
213
|
+
};
|
|
214
|
+
}
|
|
200
215
|
|
|
201
216
|
return result;
|
|
202
217
|
}
|
package/src/core/execute.ts
CHANGED
|
@@ -31,6 +31,14 @@ function expandTilde(path: string): string {
|
|
|
31
31
|
|
|
32
32
|
export type ExecuteCaptureMode = 'inherit' | 'pipe';
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Permission scope levels for tool execution
|
|
36
|
+
* - read: Only allow read operations (no mutations)
|
|
37
|
+
* - write: Allow write operations but no destructive actions
|
|
38
|
+
* - full: Full access (default)
|
|
39
|
+
*/
|
|
40
|
+
export type ScopeLevel = 'read' | 'write' | 'full';
|
|
41
|
+
|
|
34
42
|
export interface ExecuteToolOptions {
|
|
35
43
|
packageName: string;
|
|
36
44
|
command?: string;
|
|
@@ -41,6 +49,10 @@ export interface ExecuteToolOptions {
|
|
|
41
49
|
capture: ExecuteCaptureMode;
|
|
42
50
|
timeoutMs?: number;
|
|
43
51
|
teeStderr?: boolean;
|
|
52
|
+
/** Permission scope for the tool */
|
|
53
|
+
scope?: ScopeLevel;
|
|
54
|
+
/** Run in sandboxed environment with restricted file system access */
|
|
55
|
+
sandbox?: boolean;
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
export interface ExecuteToolResult {
|
|
@@ -348,6 +360,39 @@ function buildRuntimeCommand(entryPath: string, cmdArgs: string[]): { execCmd: s
|
|
|
348
360
|
return { execCmd: 'node', execArgs: [entryPath, ...cmdArgs], runtime: 'node' };
|
|
349
361
|
}
|
|
350
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Build security environment variables for scope and sandbox restrictions
|
|
365
|
+
*/
|
|
366
|
+
function buildSecurityEnv(
|
|
367
|
+
scope: ScopeLevel,
|
|
368
|
+
sandbox: boolean,
|
|
369
|
+
cwd: string
|
|
370
|
+
): Record<string, string> {
|
|
371
|
+
const env: Record<string, string> = {};
|
|
372
|
+
|
|
373
|
+
// Set scope environment variable for tools to respect
|
|
374
|
+
env.CLI4AI_SCOPE = scope;
|
|
375
|
+
|
|
376
|
+
// Sandbox restrictions
|
|
377
|
+
if (sandbox) {
|
|
378
|
+
env.CLI4AI_SANDBOX = '1';
|
|
379
|
+
|
|
380
|
+
// Restrict file system access to temp directories and package directory
|
|
381
|
+
// Tools should check these env vars and restrict their operations
|
|
382
|
+
const tmpDir = process.env.TMPDIR || process.env.TMP || process.env.TEMP || '/tmp';
|
|
383
|
+
env.CLI4AI_SANDBOX_ALLOWED_PATHS = [
|
|
384
|
+
tmpDir,
|
|
385
|
+
cwd, // Allow access to current working directory
|
|
386
|
+
].join(':');
|
|
387
|
+
|
|
388
|
+
// Restrict network access in sandbox mode
|
|
389
|
+
// Tools should check this and limit network operations
|
|
390
|
+
env.CLI4AI_SANDBOX_NETWORK = 'restricted';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return env;
|
|
394
|
+
}
|
|
395
|
+
|
|
351
396
|
async function ensureRuntimeAvailable(): Promise<void> {
|
|
352
397
|
if (!commandExists('node')) {
|
|
353
398
|
log('⚠️ Node.js is required to run this tool\n');
|
|
@@ -409,6 +454,19 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
|
|
|
409
454
|
|
|
410
455
|
const teeStderr = options.teeStderr ?? true;
|
|
411
456
|
|
|
457
|
+
// Build security environment for scope and sandbox
|
|
458
|
+
const scope = options.scope ?? 'full';
|
|
459
|
+
const sandbox = options.sandbox ?? false;
|
|
460
|
+
const securityEnv = buildSecurityEnv(scope, sandbox, invocationDir);
|
|
461
|
+
|
|
462
|
+
// Log security restrictions if active
|
|
463
|
+
if (scope !== 'full' || sandbox) {
|
|
464
|
+
const restrictions: string[] = [];
|
|
465
|
+
if (scope !== 'full') restrictions.push(`scope=${scope}`);
|
|
466
|
+
if (sandbox) restrictions.push('sandbox=enabled');
|
|
467
|
+
log(`🔒 Security: ${restrictions.join(', ')}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
412
470
|
if (options.capture === 'inherit') {
|
|
413
471
|
const proc = spawn(execCmd, execArgs, {
|
|
414
472
|
stdio: 'inherit',
|
|
@@ -421,6 +479,7 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
|
|
|
421
479
|
C4AI_PACKAGE_NAME: pkg.name,
|
|
422
480
|
C4AI_ENTRY: entryPath,
|
|
423
481
|
...secretsEnv,
|
|
482
|
+
...securityEnv,
|
|
424
483
|
...(options.env ?? {})
|
|
425
484
|
}
|
|
426
485
|
});
|
|
@@ -453,6 +512,7 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
|
|
|
453
512
|
C4AI_PACKAGE_NAME: pkg.name,
|
|
454
513
|
C4AI_ENTRY: entryPath,
|
|
455
514
|
...secretsEnv,
|
|
515
|
+
...securityEnv,
|
|
456
516
|
...(options.env ?? {})
|
|
457
517
|
}
|
|
458
518
|
});
|
package/src/mcp/server.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { homedir } from 'os';
|
|
|
17
17
|
import { type Manifest } from '../core/manifest.js';
|
|
18
18
|
import { manifestToMcpTools, type McpTool } from './adapter.js';
|
|
19
19
|
import { getSecret } from '../core/secrets.js';
|
|
20
|
+
import { loadConfig } from '../core/config.js';
|
|
20
21
|
|
|
21
22
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
23
|
// SECURITY: Audit logging and rate limiting
|
|
@@ -33,6 +34,7 @@ interface RateLimitEntry {
|
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Audit log an MCP tool call for security tracking
|
|
37
|
+
* Can be disabled via `cli4ai config set audit.enabled false`
|
|
36
38
|
*/
|
|
37
39
|
function auditLog(
|
|
38
40
|
packageName: string,
|
|
@@ -42,6 +44,12 @@ function auditLog(
|
|
|
42
44
|
errorMessage?: string
|
|
43
45
|
): void {
|
|
44
46
|
try {
|
|
47
|
+
// Check if audit logging is enabled
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
if (!config.audit?.enabled) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
if (!existsSync(AUDIT_LOG_DIR)) {
|
|
46
54
|
mkdirSync(AUDIT_LOG_DIR, { recursive: true });
|
|
47
55
|
}
|