File size: 6,075 Bytes
64bc9a3
 
 
 
fe11cca
fce80d1
fe11cca
64bc9a3
20765cf
 
fe11cca
 
 
64bc9a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe11cca
64bc9a3
fe11cca
 
 
 
dfd5fc7
 
 
 
 
 
 
 
fce80d1
64bc9a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe11cca
64bc9a3
 
 
fe11cca
 
64bc9a3
fe11cca
64bc9a3
 
 
 
 
 
 
 
fe11cca
 
 
 
 
64bc9a3
fe11cca
 
64bc9a3
fe11cca
 
 
64bc9a3
fe11cca
 
fce80d1
 
64bc9a3
fe11cca
 
64bc9a3
fe11cca
 
 
 
 
 
 
 
 
 
 
 
64bc9a3
fe11cca
 
 
 
64bc9a3
fe11cca
 
 
 
 
64bc9a3
 
 
 
 
 
fe11cca
fce80d1
 
 
20765cf
 
 
fce80d1
 
20765cf
fce80d1
 
 
20765cf
fce80d1
 
 
 
20765cf
 
fce80d1
 
 
 
fe11cca
 
 
 
 
64bc9a3
fe11cca
 
 
 
 
 
 
64bc9a3
fe11cca
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// Sandboxed shell tool β€” multi-OS (macOS, Linux, Windows).
// Detects platform and uses the appropriate shell.
// Only allowlisted commands run by default.
// Set MONA_ALLOW_CMDS to extend. Use MONA_SHELL_UNSAFE=1 to allow anything.

import { exec, spawn } from 'node:child_process';
import { promisify } from 'node:util';
import os from 'node:os';
import fs from 'node:fs';
import path from 'node:path';

const pexec = promisify(exec);

// ── Platform detection ────────────────────────────────────────────
const PLATFORM = os.platform(); // 'darwin' | 'linux' | 'win32'

const SHELL_CONFIG = {
  darwin: {
    shell: '/bin/zsh',
    path:  '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin',
  },
  linux: {
    shell: '/bin/sh',
    path:  '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
  },
  win32: {
    shell: 'powershell.exe',
    path:  '', // Windows uses system PATH
  },
};

const cfg = SHELL_CONFIG[PLATFORM] || SHELL_CONFIG.linux;

// ── OS-aware default allowlist ────────────────────────────────────
const DEFAULTS = {
  darwin: 'df,uptime,uname,whoami,date,hostname,vm_stat,top,cat,head,tail,wc,ls,pwd,echo,env,which,sw_vers,sysctl',
  linux:  'df,uptime,uname,whoami,date,hostname,free,ps,top,cat,head,tail,wc,ls,pwd,echo,env,which',
  win32:  'whoami,date,hostname,dir,type,echo,ver,systeminfo,tasklist',
};

const ALLOW = new Set(
  (process.env.MONA_ALLOW_CMDS || DEFAULTS[PLATFORM] || DEFAULTS.linux)
    .split(',').map(s => s.trim()).filter(Boolean)
);
const UNSAFE = process.env.MONA_SHELL_UNSAFE === '1';

/** Shell security posture β€” advertised to the cloud in `hello` so the
 *  control plane can enforce agent_permissions without probing. */
export const security = {
  allowlist: [...ALLOW].sort(),
  unsafe: UNSAFE,
  platform: PLATFORM,
};

// ── Per-OS command mapping (translate common unix  windows) ──────
const CMD_MAP_WIN32 = {
  ls: 'dir',
  cat: 'type',
  pwd: 'echo %cd%',
  uname: 'ver',
  df: 'wmic logicaldisk get size,freespace,caption',
  free: 'systeminfo | find "Available Physical Memory"',
  uptime: 'systeminfo | find "System Boot Time"',
  whoami: 'whoami',
  clear: 'cls',
  cp: 'copy',
  mv: 'move',
  rm: 'del',
};

// ── Blocked patterns (always denied) ──────────────────────────────
const BLOCKED_PATTERNS = [
  // Unix
  /rm\s+(-[a-z]*[rf][a-z]*\s+)+\/s*$/i,
  /rm\s+(-[a-z]*[rf][a-z]*\s+)+\*\s*$/i,
  /mkfs\b/i,
  /dd\s+if=/i,
  /:\(\)\s*\{.*\}/,
  />\s*\/dev\/sd[a-z]/i,
  /chmod\s+777\s+\//i,
  /sudo\b/i,
  /shutdown\b/i,
  // Windows
  /format\s+[a-z]:/i,
  /del\s+\/f\s+\/s\s+[a-z]:\\/i,
  /rmdir\s+\/s\s+[a-z]:\\/i,
  /diskpart\b/i,
];

const EXEC_OPTS = {
  timeout:   15_000,
  maxBuffer: 1 << 20, // 1 MB
  shell:     cfg.shell,
  env: {
    ...process.env,
    PATH: cfg.path || process.env.PATH,
  },
};

// ── Tool definition ───────────────────────────────────────────────
export const shell = {
  name: 'shell',
  description: `Execute a shell command (${PLATFORM}; allowlisted by default; max 15s timeout; background:true for GUI/long-running processes)`,
  args: { cmd: 'string β€” the command to run', background: 'bool β€” optional, detach and return immediately (for GUI apps, servers, tkinter windows)' },
  platform: PLATFORM,

  async run(args) {
    let cmd = String(args.cmd || '').trim();
    if (!cmd) return { error: 'Empty command' };
    if (cmd.length > 2000) return { error: 'Command too long (max 2000 chars)' };

    // Block dangerous patterns always
    for (const pat of BLOCKED_PATTERNS) {
      if (pat.test(cmd)) {
        return { error: 'Command blocked for security', cmd };
      }
    }

    // Allowlist check (unless unsafe mode)
    if (!UNSAFE) {
      const base = cmd.split(/[;\s|&]/)[0].trim().split('/').pop().split('\\').pop();
      if (!ALLOW.has(base)) {
        return {
          error: `Command '${base}' not in allowlist`,
          allowed: [...ALLOW].sort(),
          platform: PLATFORM,
          hint: 'Set MONA_ALLOW_CMDS to extend',
        };
      }
    }

    // Map unix commands to Windows equivalents when on win32
    if (PLATFORM === 'win32' && CMD_MAP_WIN32[cmd.split(/\s+/)[0].toLowerCase()]) {
      const [orig, ...rest] = cmd.split(/\s+/);
      cmd = CMD_MAP_WIN32[orig.toLowerCase()] + (rest.length ? ' ' + rest.join(' ') : '');
    }

    try {
      // Background mode: GUI apps / long-running processes (e.g. tkinter
      // windows) must not block the task or die with the 15s timeout.
      if (args.background) {
        const logFile = path.join(os.homedir(), '.mona-agent', `bg-${Date.now()}.log`);
        fs.mkdirSync(path.dirname(logFile), { recursive: true });
        const out = fs.openSync(logFile, 'a');
        const child = spawn(cfg.shell, ['-c', cmd], {
          detached: true,
          stdio: ['ignore', out, out],
          env: { ...process.env, PATH: cfg.path || process.env.PATH },
        });
        child.unref();
        fs.closeSync(out);
        return {
          exitCode: null,
          pid: child.pid,
          background: true,
          log: logFile,
          note: 'Process started in background and detached from the agent. Output: ' + logFile,
          platform: PLATFORM,
        };
      }

      const { stdout, stderr } = await pexec(cmd, EXEC_OPTS);
      return {
        exitCode: 0,
        stdout: stdout.slice(0, 8000),
        stderr: stderr.slice(0, 2000),
        platform: PLATFORM,
      };
    } catch (err) {
      return {
        exitCode: err.code ?? 1,
        stdout:   (err.stdout || '').slice(0, 8000),
        stderr:   (err.stderr || '').slice(0, 2000),
        error:    err.killed ? 'Command timed out (15s)' : err.message,
        platform: PLATFORM,
      };
    }
  },
};