cli4ai 1.0.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli4ai",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "The package manager for AI CLI tools - cli4ai.com",
5
5
  "type": "module",
6
6
  "bin": {
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
 
@@ -16,9 +16,8 @@ import {
16
16
  LOCAL_PACKAGES_DIR,
17
17
  loadConfig
18
18
  } from '../core/config.js';
19
- import { lockPackage, computeDirectoryIntegrity, type LockedPackage } from '../core/lockfile.js';
19
+ import { lockPackage, type LockedPackage } from '../core/lockfile.js';
20
20
  import { linkPackageDirect, isBinInPath, getPathInstructions } from '../core/link.js';
21
- import { getRegistryIntegrity, verifyPackageIntegrity } from '../core/registry.js';
22
21
 
23
22
  interface AddOptions {
24
23
  local?: boolean;
@@ -417,27 +416,6 @@ export async function addCommand(packages: string[], options: AddOptions): Promi
417
416
  await installNpmDependencies(result.path, plan.manifest.dependencies);
418
417
  }
419
418
 
420
- // SECURITY: Verify integrity against registry
421
- let integrity: string | undefined;
422
- try {
423
- const verification = await verifyPackageIntegrity(result.name, result.path);
424
- integrity = verification.actual;
425
-
426
- if (verification.expected) {
427
- if (verification.valid) {
428
- log(` integrity: ${integrity.slice(0, 20)}... ✓`);
429
- } else {
430
- log(` ⚠️ Integrity mismatch!`);
431
- log(` Expected: ${verification.expected.slice(0, 30)}...`);
432
- log(` Actual: ${verification.actual.slice(0, 30)}...`);
433
- }
434
- } else {
435
- log(` integrity: ${integrity.slice(0, 20)}...`);
436
- }
437
- } catch (err) {
438
- log(` warning: could not compute integrity hash`);
439
- }
440
-
441
419
  // Link to PATH for global installs
442
420
  if (options.global) {
443
421
  result.binPath = linkPackageDirect(plan.manifest, result.path);
@@ -449,8 +427,7 @@ export async function addCommand(packages: string[], options: AddOptions): Promi
449
427
  const lockedPkg: LockedPackage = {
450
428
  name: result.name,
451
429
  version: result.version,
452
- resolved: result.source === 'local' ? result.path : result.path,
453
- integrity
430
+ resolved: result.source === 'local' ? result.path : result.path
454
431
  };
455
432
  lockPackage(projectDir, lockedPkg);
456
433
  }
@@ -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;
@@ -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
  }
@@ -15,7 +15,6 @@ import { log } from '../lib/cli.js';
15
15
  import { findPackage } from './config.js';
16
16
  import { loadManifest, type Manifest } from './manifest.js';
17
17
  import { getSecret } from './secrets.js';
18
- import { checkPackageIntegrity } from './lockfile.js';
19
18
 
20
19
  /**
21
20
  * Expand ~ to home directory in paths
@@ -32,6 +31,14 @@ function expandTilde(path: string): string {
32
31
 
33
32
  export type ExecuteCaptureMode = 'inherit' | 'pipe';
34
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
+
35
42
  export interface ExecuteToolOptions {
36
43
  packageName: string;
37
44
  command?: string;
@@ -42,6 +49,10 @@ export interface ExecuteToolOptions {
42
49
  capture: ExecuteCaptureMode;
43
50
  timeoutMs?: number;
44
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;
45
56
  }
46
57
 
47
58
  export interface ExecuteToolResult {
@@ -349,6 +360,39 @@ function buildRuntimeCommand(entryPath: string, cmdArgs: string[]): { execCmd: s
349
360
  return { execCmd: 'node', execArgs: [entryPath, ...cmdArgs], runtime: 'node' };
350
361
  }
351
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
+
352
396
  async function ensureRuntimeAvailable(): Promise<void> {
353
397
  if (!commandExists('node')) {
354
398
  log('⚠️ Node.js is required to run this tool\n');
@@ -389,27 +433,6 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
389
433
 
390
434
  const manifest = loadManifest(pkg.path);
391
435
 
392
- const integrityCheck = checkPackageIntegrity(invocationDir, options.packageName, pkg.path);
393
- if (!integrityCheck.valid) {
394
- log(`\n⚠️ SECURITY WARNING: Package integrity check failed for ${options.packageName}`);
395
- if (integrityCheck.error) log(` ${integrityCheck.error}`);
396
- if (integrityCheck.expected && integrityCheck.actual) {
397
- log(` Expected: ${integrityCheck.expected.slice(0, 30)}...`);
398
- log(` Actual: ${integrityCheck.actual.slice(0, 30)}...`);
399
- }
400
- log(`\n The package may have been tampered with or modified since installation.`);
401
- log(` To fix: reinstall with "cli4ai remove ${options.packageName} && cli4ai add ${options.packageName}"\n`);
402
-
403
- const shouldContinue = await confirm('Continue anyway? (NOT RECOMMENDED)');
404
- if (!shouldContinue) {
405
- throw new ExecuteToolError('INTEGRITY_ERROR', 'Package integrity verification failed', {
406
- package: options.packageName,
407
- hint: `Reinstall the package with: cli4ai remove ${options.packageName} && cli4ai add ${options.packageName}`
408
- });
409
- }
410
- log('');
411
- }
412
-
413
436
  await ensureRuntimeAvailable();
414
437
 
415
438
  await checkPeerDependencies(pkg.path);
@@ -431,6 +454,19 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
431
454
 
432
455
  const teeStderr = options.teeStderr ?? true;
433
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
+
434
470
  if (options.capture === 'inherit') {
435
471
  const proc = spawn(execCmd, execArgs, {
436
472
  stdio: 'inherit',
@@ -443,6 +479,7 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
443
479
  C4AI_PACKAGE_NAME: pkg.name,
444
480
  C4AI_ENTRY: entryPath,
445
481
  ...secretsEnv,
482
+ ...securityEnv,
446
483
  ...(options.env ?? {})
447
484
  }
448
485
  });
@@ -475,6 +512,7 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
475
512
  C4AI_PACKAGE_NAME: pkg.name,
476
513
  C4AI_ENTRY: entryPath,
477
514
  ...secretsEnv,
515
+ ...securityEnv,
478
516
  ...(options.env ?? {})
479
517
  }
480
518
  });
@@ -3,14 +3,10 @@
3
3
  *
4
4
  * Tracks installed packages with exact versions and sources
5
5
  * for reproducible installations.
6
- *
7
- * SECURITY: Includes SRI (Subresource Integrity) hash verification
8
- * to detect tampered packages.
9
6
  */
10
7
 
11
- import { readFileSync, writeFileSync, renameSync, existsSync, readdirSync, statSync } from 'fs';
12
- import { resolve, join, relative } from 'path';
13
- import { createHash } from 'crypto';
8
+ import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
9
+ import { resolve } from 'path';
14
10
 
15
11
  // ═══════════════════════════════════════════════════════════════════════════
16
12
  // TYPES
@@ -36,125 +32,6 @@ export interface Lockfile {
36
32
  export const LOCKFILE_NAME = 'cli4ai.lock';
37
33
  export const LOCKFILE_VERSION = 1;
38
34
 
39
- // ═══════════════════════════════════════════════════════════════════════════
40
- // INTEGRITY HASHING (SRI - Subresource Integrity)
41
- // ═══════════════════════════════════════════════════════════════════════════
42
-
43
- /**
44
- * Compute SHA-512 hash of a file and return SRI format string
45
- */
46
- export function computeFileIntegrity(filePath: string): string {
47
- const content = readFileSync(filePath);
48
- const hash = createHash('sha512').update(content).digest('base64');
49
- return `sha512-${hash}`;
50
- }
51
-
52
- /**
53
- * Compute SHA-512 hash of a directory's contents (deterministic).
54
- * Hashes all files in sorted order to produce a reproducible hash.
55
- */
56
- export function computeDirectoryIntegrity(dirPath: string): string {
57
- const hash = createHash('sha512');
58
- const files: string[] = [];
59
-
60
- // Recursively collect all files
61
- function collectFiles(dir: string): void {
62
- const entries = readdirSync(dir, { withFileTypes: true });
63
- for (const entry of entries) {
64
- // Skip node_modules and hidden directories
65
- if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
66
-
67
- const fullPath = join(dir, entry.name);
68
- if (entry.isDirectory()) {
69
- collectFiles(fullPath);
70
- } else if (entry.isFile()) {
71
- files.push(fullPath);
72
- }
73
- }
74
- }
75
-
76
- collectFiles(dirPath);
77
-
78
- // Sort files for deterministic ordering
79
- files.sort();
80
-
81
- // Hash each file's relative path and content
82
- for (const filePath of files) {
83
- const relativePath = relative(dirPath, filePath);
84
- const content = readFileSync(filePath);
85
-
86
- // Include path in hash for structural integrity
87
- hash.update(relativePath);
88
- hash.update(content);
89
- }
90
-
91
- return `sha512-${hash.digest('base64')}`;
92
- }
93
-
94
- /**
95
- * Verify a package's integrity against stored hash
96
- */
97
- export function verifyIntegrity(dirPath: string, expectedIntegrity: string): boolean {
98
- if (!expectedIntegrity) return true; // No integrity check if not stored
99
-
100
- try {
101
- const actualIntegrity = computeDirectoryIntegrity(dirPath);
102
- return actualIntegrity === expectedIntegrity;
103
- } catch {
104
- return false;
105
- }
106
- }
107
-
108
- /**
109
- * Result of integrity verification
110
- */
111
- export interface IntegrityCheckResult {
112
- valid: boolean;
113
- expected?: string;
114
- actual?: string;
115
- error?: string;
116
- }
117
-
118
- /**
119
- * Check integrity of a locked package
120
- */
121
- export function checkPackageIntegrity(
122
- projectDir: string,
123
- packageName: string,
124
- packagePath: string
125
- ): IntegrityCheckResult {
126
- const locked = getLockedPackage(projectDir, packageName);
127
-
128
- if (!locked) {
129
- return { valid: true }; // No lock entry, can't verify
130
- }
131
-
132
- if (!locked.integrity) {
133
- return { valid: true }; // No integrity hash stored
134
- }
135
-
136
- try {
137
- const actualIntegrity = computeDirectoryIntegrity(packagePath);
138
-
139
- if (actualIntegrity === locked.integrity) {
140
- return { valid: true, expected: locked.integrity, actual: actualIntegrity };
141
- }
142
-
143
- return {
144
- valid: false,
145
- expected: locked.integrity,
146
- actual: actualIntegrity,
147
- error: 'Integrity mismatch - package may have been tampered with'
148
- };
149
- } catch (err) {
150
- return {
151
- valid: false,
152
- expected: locked.integrity,
153
- error: `Failed to compute integrity: ${err instanceof Error ? err.message : String(err)}`
154
- };
155
- }
156
- }
157
-
158
35
  // ═══════════════════════════════════════════════════════════════════════════
159
36
  // FUNCTIONS
160
37
  // ═══════════════════════════════════════════════════════════════════════════
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
  }