@pheem49/mint 1.5.0 → 1.5.2

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.
Files changed (101) hide show
  1. package/README.md +35 -1
  2. package/main.js +28 -14
  3. package/mint-cli-logic.js +3 -119
  4. package/mint-cli.js +201 -500
  5. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  6. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +40 -0
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json +15 -0
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  10. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +15 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  24. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +23 -0
  25. package/package.json +40 -17
  26. package/src/AI_Brain/Gemini_API.js +147 -46
  27. package/src/AI_Brain/autonomous_brain.js +2 -1
  28. package/src/AI_Brain/memory_store.js +299 -3
  29. package/src/AI_Brain/proactive_engine.js +12 -2
  30. package/src/Automation_Layer/browser_automation.js +26 -24
  31. package/src/CLI/approval_handler.js +42 -0
  32. package/src/CLI/chat_router.js +18 -6
  33. package/src/CLI/chat_ui.js +583 -52
  34. package/src/CLI/cli_colors.js +32 -0
  35. package/src/CLI/cli_formatters.js +89 -0
  36. package/src/CLI/code_agent.js +369 -71
  37. package/src/CLI/image_input.js +90 -0
  38. package/src/CLI/intent_detectors.js +181 -0
  39. package/src/CLI/interactive_chat.js +479 -0
  40. package/src/CLI/list_features.js +3 -0
  41. package/src/CLI/onboarding.js +72 -15
  42. package/src/CLI/repo_summarizer.js +282 -0
  43. package/src/CLI/semantic_code_search.js +312 -0
  44. package/src/CLI/skill_manager.js +41 -0
  45. package/src/CLI/slash_command_handler.js +418 -0
  46. package/src/CLI/symbol_indexer.js +231 -0
  47. package/src/CLI/updater.js +6 -4
  48. package/src/Channels/discord_bridge.js +11 -13
  49. package/src/Channels/line_bridge.js +10 -10
  50. package/src/Channels/slack_bridge.js +7 -12
  51. package/src/Channels/telegram_bridge.js +6 -14
  52. package/src/Channels/whatsapp_bridge.js +11 -9
  53. package/src/System/action_executor.js +59 -10
  54. package/src/System/chat_history_manager.js +20 -12
  55. package/src/System/config_manager.js +31 -1
  56. package/src/System/granular_automation.js +122 -53
  57. package/src/System/optional_require.js +23 -0
  58. package/src/System/proactive_loop.js +19 -3
  59. package/src/System/safety_manager.js +108 -0
  60. package/src/System/sandbox_runner.js +182 -0
  61. package/src/System/system_automation.js +127 -81
  62. package/src/System/system_info.js +70 -0
  63. package/src/System/tool_registry.js +280 -0
  64. package/src/System/window_manager.js +4 -2
  65. package/src/UI/live2d_manager.js +566 -0
  66. package/src/UI/renderer.js +339 -21
  67. package/src/UI/settings.css +655 -420
  68. package/src/UI/settings.html +478 -432
  69. package/src/UI/settings.js +10 -8
  70. package/src/UI/styles.css +516 -31
  71. package/.codex +0 -0
  72. package/docs/assets/Agent_Mint.png +0 -0
  73. package/docs/assets/CLI_Screen.png +0 -0
  74. package/docs/assets/Settings.png +0 -0
  75. package/docs/assets/icon.png +0 -0
  76. package/docs/guide.html +0 -632
  77. package/docs/index.html +0 -133
  78. package/docs/style.css +0 -579
  79. package/index.html +0 -16
  80. package/src/UI/index.html +0 -126
  81. package/tech_news.txt +0 -3
  82. package/test_knowledge.txt +0 -3
  83. package/tests/action_executor_safety.test.js +0 -67
  84. package/tests/agent_orchestrator.test.js +0 -41
  85. package/tests/chat_router.test.js +0 -42
  86. package/tests/code_agent.test.js +0 -69
  87. package/tests/config_manager.test.js +0 -141
  88. package/tests/docker.test.js +0 -46
  89. package/tests/file_operations.test.js +0 -57
  90. package/tests/gmail.test.js +0 -135
  91. package/tests/gmail_auth.test.js +0 -129
  92. package/tests/google_calendar.test.js +0 -113
  93. package/tests/google_tts_urls.test.js +0 -24
  94. package/tests/memory_store.test.js +0 -185
  95. package/tests/notion.test.js +0 -121
  96. package/tests/provider_routing.test.js +0 -83
  97. package/tests/safety_manager.test.js +0 -40
  98. package/tests/spotify.test.js +0 -201
  99. package/tests/system_monitor.test.js +0 -37
  100. package/tests/updater.test.js +0 -32
  101. package/tests/workspace_manager.test.js +0 -56
@@ -1,88 +1,157 @@
1
- const { exec } = require('child_process');
1
+ const { execFile, spawnSync } = require('child_process');
2
2
  const { screen } = require('electron');
3
3
 
4
- /**
5
- * GranularAutomation handles low-level OS input via xdotool.
6
- * It uses a normalized coordinate system (0-1000).
7
- */
4
+ function commandExists(command) {
5
+ const lookup = process.platform === 'win32' ? 'where' : 'which';
6
+ const result = spawnSync(lookup, [command], { encoding: 'utf8', shell: false });
7
+ return result.status === 0;
8
+ }
9
+
10
+ function run(command, args = []) {
11
+ return new Promise((resolve, reject) => {
12
+ execFile(command, args, (err, stdout, stderr) => {
13
+ if (err) {
14
+ err.stderr = stderr;
15
+ reject(err);
16
+ return;
17
+ }
18
+ resolve(stdout);
19
+ });
20
+ });
21
+ }
22
+
23
+ function unsupported(feature) {
24
+ throw new Error(`${feature} is not supported on ${process.platform} by the current input automation provider.`);
25
+ }
26
+
27
+ function escapePowerShellSingleQuoted(value) {
28
+ return String(value || '').replace(/'/g, "''");
29
+ }
30
+
31
+ function keyToMacKey(key) {
32
+ const value = String(key || '').trim();
33
+ const map = {
34
+ Enter: 'return',
35
+ Return: 'return',
36
+ Escape: 'escape',
37
+ Esc: 'escape',
38
+ Space: 'space',
39
+ Backspace: 'delete',
40
+ Delete: 'forward delete',
41
+ Tab: 'tab'
42
+ };
43
+ return map[value] || value;
44
+ }
45
+
8
46
  class GranularAutomation {
9
47
  constructor() {
10
- this.screenWidth = 1920; // Default fallback
48
+ this.screenWidth = 1920;
11
49
  this.screenHeight = 1080;
12
50
  this.updateScreenSize();
13
51
  }
14
52
 
15
53
  updateScreenSize() {
16
54
  try {
17
- // In Electron main process, we can use the screen module
18
55
  const primaryDisplay = screen.getPrimaryDisplay();
19
56
  if (primaryDisplay && primaryDisplay.size) {
20
57
  this.screenWidth = primaryDisplay.size.width;
21
58
  this.screenHeight = primaryDisplay.size.height;
22
- console.log(`[Automation] Screen detected: ${this.screenWidth}x${this.screenHeight}`);
23
59
  }
24
- } catch (e) {
25
- // Fallback for CLI or cases where screen module is unavailable
26
- exec('xdpyinfo | grep dimensions', (err, stdout) => {
27
- if (!err && stdout) {
28
- const match = stdout.match(/(\d+)x(\d+) pixels/);
29
- if (match) {
30
- this.screenWidth = parseInt(match[1]);
31
- this.screenHeight = parseInt(match[2]);
32
- console.log(`[Automation] Screen detected via xdpyinfo: ${this.screenWidth}x${this.screenHeight}`);
33
- }
34
- }
35
- });
60
+ } catch (_) {
61
+ // Electron screen can be unavailable in CLI-only contexts.
36
62
  }
37
63
  }
38
64
 
39
65
  scaleX(x) {
40
- return Math.round((x / 1000) * this.screenWidth);
66
+ return Math.round((Number(x) / 1000) * this.screenWidth);
41
67
  }
42
68
 
43
69
  scaleY(y) {
44
- return Math.round((y / 1000) * this.screenHeight);
70
+ return Math.round((Number(y) / 1000) * this.screenHeight);
45
71
  }
46
72
 
47
- run(command) {
48
- return new Promise((resolve, reject) => {
49
- exec(command, (err, stdout, stderr) => {
50
- if (err) {
51
- console.error(`[Automation] xdotool error: ${stderr}`);
52
- reject(err);
53
- } else {
54
- resolve(stdout);
55
- }
56
- });
57
- });
73
+ provider() {
74
+ if (process.platform === 'darwin') return macProvider;
75
+ if (process.platform === 'win32') return windowsProvider;
76
+ return linuxProvider;
58
77
  }
59
78
 
60
- async mouseMove(x, y) {
61
- const sx = this.scaleX(x);
62
- const sy = this.scaleY(y);
63
- console.log(`[Automation] Moving mouse to ${sx}, ${sy}`);
64
- return this.run(`xdotool mousemove ${sx} ${sy}`);
79
+ mouseMove(x, y) {
80
+ return this.provider().mouseMove(this.scaleX(x), this.scaleY(y));
65
81
  }
66
82
 
67
- async mouseClick(x, y, button = 1) {
68
- const sx = this.scaleX(x);
69
- const sy = this.scaleY(y);
70
- console.log(`[Automation] Clicking ${button} at ${sx}, ${sy}`);
71
- // move first then click to be safe
72
- return this.run(`xdotool mousemove ${sx} ${sy} click ${button}`);
83
+ mouseClick(x, y, button = 1) {
84
+ return this.provider().mouseClick(this.scaleX(x), this.scaleY(y), button);
73
85
  }
74
86
 
75
- async typeText(text) {
76
- console.log(`[Automation] Typing: ${text}`);
77
- // Escape double quotes for shell
78
- const escaped = text.replace(/"/g, '\\"');
79
- return this.run(`xdotool type "${escaped}"`);
87
+ typeText(text) {
88
+ return this.provider().typeText(String(text || ''));
80
89
  }
81
90
 
82
- async keyTap(key) {
83
- console.log(`[Automation] Key tap: ${key}`);
84
- return this.run(`xdotool key "${key}"`);
91
+ keyTap(key) {
92
+ return this.provider().keyTap(String(key || ''));
85
93
  }
86
94
  }
87
95
 
88
- module.exports = new GranularAutomation();
96
+ const linuxProvider = {
97
+ mouseMove: (x, y) => run('xdotool', ['mousemove', String(x), String(y)]),
98
+ mouseClick: (x, y, button = 1) => run('xdotool', ['mousemove', String(x), String(y), 'click', String(button)]),
99
+ typeText: (text) => run('xdotool', ['type', text]),
100
+ keyTap: (key) => run('xdotool', ['key', key])
101
+ };
102
+
103
+ const macProvider = {
104
+ mouseMove(x, y) {
105
+ if (!commandExists('cliclick')) return unsupported('Mouse move');
106
+ return run('cliclick', [`m:${x},${y}`]);
107
+ },
108
+ mouseClick(x, y) {
109
+ if (!commandExists('cliclick')) return unsupported('Mouse click');
110
+ return run('cliclick', [`c:${x},${y}`]);
111
+ },
112
+ typeText(text) {
113
+ if (commandExists('cliclick')) return run('cliclick', [`t:${text}`]);
114
+ return run('osascript', ['-e', `tell application "System Events" to keystroke ${JSON.stringify(text)}`]);
115
+ },
116
+ keyTap(key) {
117
+ const macKey = keyToMacKey(key);
118
+ if (commandExists('cliclick')) return run('cliclick', [`kp:${macKey}`]);
119
+ if (macKey.length === 1) {
120
+ return run('osascript', ['-e', `tell application "System Events" to keystroke ${JSON.stringify(macKey)}`]);
121
+ }
122
+ return unsupported('Special key tap without cliclick');
123
+ }
124
+ };
125
+
126
+ const windowsProvider = {
127
+ mouseMove(x, y) {
128
+ const script = `[void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y})`;
129
+ return run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
130
+ },
131
+ mouseClick(x, y, button = 1) {
132
+ const down = Number(button) === 2 ? '0x0008' : '0x0002';
133
+ const up = Number(button) === 2 ? '0x0010' : '0x0004';
134
+ const script = [
135
+ "Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X,int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(int dwFlags,int dx,int dy,int dwData,int dwExtraInfo);' -Name NativeMouse -Namespace Mint;",
136
+ `[Mint.NativeMouse]::SetCursorPos(${x}, ${y}) | Out-Null;`,
137
+ `[Mint.NativeMouse]::mouse_event(${down},0,0,0,0);`,
138
+ `[Mint.NativeMouse]::mouse_event(${up},0,0,0,0);`
139
+ ].join(' ');
140
+ return run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
141
+ },
142
+ typeText(text) {
143
+ const safe = escapePowerShellSingleQuoted(text);
144
+ const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${safe}')`;
145
+ return run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
146
+ },
147
+ keyTap(key) {
148
+ const safe = escapePowerShellSingleQuoted(key);
149
+ const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{${safe}}')`;
150
+ return run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
151
+ }
152
+ };
153
+
154
+ const instance = new GranularAutomation();
155
+ instance._providers = { linuxProvider, macProvider, windowsProvider, commandExists };
156
+
157
+ module.exports = instance;
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Helper: ลอง require package แบบ dynamic
5
+ * ถ้าหาไม่เจอให้ throw Error พร้อม install guide
6
+ */
7
+ function requireOptional(pkg, installHint) {
8
+ try {
9
+ return require(pkg);
10
+ } catch (e) {
11
+ if (e.code === 'MODULE_NOT_FOUND') {
12
+ const hint = installHint || `npm install ${pkg}`;
13
+ throw new Error(
14
+ `[Mint] Optional package "${pkg}" is not installed.\n` +
15
+ `To use this feature, run: ${hint}\n` +
16
+ `(This package is not bundled by default to keep Mint lightweight.)`
17
+ );
18
+ }
19
+ throw e;
20
+ }
21
+ }
22
+
23
+ module.exports = { requireOptional };
@@ -1,10 +1,24 @@
1
1
  const { BrowserWindow, desktopCapturer, screen, powerMonitor } = require('electron');
2
2
  const path = require('path');
3
- const { analyzeAndSuggest } = require('../AI_Brain/proactive_engine');
4
- const { recordBehavior, getBehaviorSummary } = require('../AI_Brain/behavior_memory');
5
3
 
6
4
  const IDLE_THRESHOLD_SEC = 300;
7
5
 
6
+ let proactiveEngine = null;
7
+ function getProactiveEngine() {
8
+ if (!proactiveEngine) {
9
+ proactiveEngine = require('../AI_Brain/proactive_engine');
10
+ }
11
+ return proactiveEngine;
12
+ }
13
+
14
+ let behaviorMemory = null;
15
+ function getBehaviorMemory() {
16
+ if (!behaviorMemory) {
17
+ behaviorMemory = require('../AI_Brain/behavior_memory');
18
+ }
19
+ return behaviorMemory;
20
+ }
21
+
8
22
  function createProactiveLoop({ app, projectRoot, readConfig, getMainWindow }) {
9
23
  let proactiveGlowWindow = null;
10
24
  let proactiveIntervalHandle = null;
@@ -29,6 +43,8 @@ function createProactiveLoop({ app, projectRoot, readConfig, getMainWindow }) {
29
43
  if (!primarySource || !primarySource.thumbnail) return;
30
44
 
31
45
  const base64Image = primarySource.thumbnail.toJPEG(60).toString('base64');
46
+ const { analyzeAndSuggest } = getProactiveEngine();
47
+ const { recordBehavior, getBehaviorSummary } = getBehaviorMemory();
32
48
  const result = await analyzeAndSuggest(base64Image, getBehaviorSummary());
33
49
 
34
50
  if (result && result.message && Array.isArray(result.suggestions)) {
@@ -130,7 +146,7 @@ function createProactiveLoop({ app, projectRoot, readConfig, getMainWindow }) {
130
146
  stop,
131
147
  startIdleWatcher,
132
148
  isRunning: () => Boolean(proactiveIntervalHandle),
133
- recordBehavior
149
+ recordBehavior: (...args) => getBehaviorMemory().recordBehavior(...args)
134
150
  };
135
151
  }
136
152
 
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
+ const { readConfig } = require('./config_manager');
4
5
 
5
6
  const TIERS = Object.freeze({
6
7
  SAFE: 'safe',
@@ -40,6 +41,7 @@ const SAFE_ACTIONS = new Set([
40
41
  'clipboard_write',
41
42
  'learn_file',
42
43
  'learn_folder',
44
+ 'system_info',
43
45
  'mcp_tool',
44
46
  'mouse_move',
45
47
  'mouse_click',
@@ -56,6 +58,103 @@ function normalizeCommand(command) {
56
58
  return String(command || '').replace(/\s+/g, ' ').trim();
57
59
  }
58
60
 
61
+ function expandHome(targetPath) {
62
+ const value = String(targetPath || '');
63
+ if (value === '~') return os.homedir();
64
+ if (value.startsWith('~/')) return path.join(os.homedir(), value.slice(2));
65
+ return value;
66
+ }
67
+
68
+ function normalizeRootList(paths) {
69
+ return (Array.isArray(paths) ? paths : [])
70
+ .filter(Boolean)
71
+ .map((entry) => path.resolve(expandHome(entry)));
72
+ }
73
+
74
+ function getPolicy(config = readConfig()) {
75
+ const enabled = config.safetyEnabled !== false;
76
+ const fallbackRead = [
77
+ os.homedir(),
78
+ process.cwd(),
79
+ path.join(os.homedir(), 'Desktop'),
80
+ path.join(os.homedir(), 'Documents'),
81
+ path.join(os.homedir(), 'Downloads'),
82
+ path.join(os.homedir(), 'Pictures'),
83
+ path.join(os.homedir(), 'Music'),
84
+ path.join(os.homedir(), 'Videos')
85
+ ];
86
+ const fallbackWrite = [
87
+ os.homedir(),
88
+ process.cwd(),
89
+ path.join(os.homedir(), 'Desktop'),
90
+ path.join(os.homedir(), 'Documents'),
91
+ path.join(os.homedir(), 'Downloads'),
92
+ path.join(os.homedir(), 'Pictures'),
93
+ path.join(os.homedir(), 'Music'),
94
+ path.join(os.homedir(), 'Videos')
95
+ ];
96
+
97
+ return {
98
+ enabled,
99
+ sandboxMode: ['off', 'prefer', 'enforce'].includes(config.sandboxMode) ? config.sandboxMode : 'prefer',
100
+ sandboxCommand: config.sandboxCommand || (process.platform === 'darwin' ? 'sandbox-exec' : process.platform === 'linux' ? 'bwrap' : ''),
101
+ allowedReadPaths: normalizeRootList(config.allowedReadPaths && config.allowedReadPaths.length ? config.allowedReadPaths : fallbackRead),
102
+ allowedWritePaths: normalizeRootList(config.allowedWritePaths && config.allowedWritePaths.length ? config.allowedWritePaths : fallbackWrite),
103
+ blockedPaths: normalizeRootList(config.blockedPaths || []),
104
+ blockedFileNames: new Set(Array.isArray(config.blockedFileNames) ? config.blockedFileNames : ['.env', 'id_rsa', 'id_ed25519'])
105
+ };
106
+ }
107
+
108
+ function isPathWithin(root, targetPath) {
109
+ const relative = path.relative(root, targetPath);
110
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
111
+ }
112
+
113
+ function resolveCapabilityPath(targetPath, options = {}) {
114
+ if (!targetPath) throw new Error('Target path is required.');
115
+
116
+ const expanded = expandHome(targetPath);
117
+ if (path.isAbsolute(expanded)) return path.resolve(expanded);
118
+
119
+ const firstPart = String(expanded).split(/[/\\]/)[0];
120
+ const commonHomeFolders = new Set(['Desktop', 'Documents', 'Downloads', 'Pictures', 'Music', 'Videos']);
121
+ if (commonHomeFolders.has(firstPart)) {
122
+ return path.resolve(os.homedir(), expanded);
123
+ }
124
+
125
+ const base = options.defaultBase || process.cwd();
126
+ return path.resolve(base, expanded);
127
+ }
128
+
129
+ function assertPathCapability(targetPath, capability = 'read', options = {}) {
130
+ const policy = getPolicy(options.config);
131
+ const resolved = resolveCapabilityPath(targetPath, options);
132
+
133
+ if (!policy.enabled) return resolved;
134
+
135
+ if (policy.blockedFileNames.has(path.basename(resolved))) {
136
+ throw new Error(`Blocked ${capability} access to sensitive file name: ${path.basename(resolved)}`);
137
+ }
138
+
139
+ const blockedRoot = policy.blockedPaths.find((root) => isPathWithin(root, resolved));
140
+ if (blockedRoot) {
141
+ throw new Error(`Blocked ${capability} access to protected path: ${resolved}`);
142
+ }
143
+
144
+ const allowedRoots = capability === 'write' ? policy.allowedWritePaths : policy.allowedReadPaths;
145
+ const allowed = allowedRoots.some((root) => isPathWithin(root, resolved));
146
+ if (!allowed) {
147
+ throw new Error(`Path ${capability} denied by capability policy: ${resolved}`);
148
+ }
149
+
150
+ return resolved;
151
+ }
152
+
153
+ function getAllowedRoots(capability = 'read', options = {}) {
154
+ const policy = getPolicy(options.config);
155
+ return capability === 'write' ? policy.allowedWritePaths : policy.allowedReadPaths;
156
+ }
157
+
59
158
  function classifyShellCommand(command) {
60
159
  const normalized = normalizeCommand(command);
61
160
  if (!normalized) {
@@ -105,6 +204,7 @@ function classifyAction(action = {}) {
105
204
  function assertActionAllowed(action, options = {}) {
106
205
  const classification = classifyAction(action);
107
206
  const allowDangerous = options.allowDangerous === true;
207
+ const allowApproval = options.allowApproval === true;
108
208
 
109
209
  if (classification.tier === TIERS.BLOCKED) {
110
210
  throw new Error(`Blocked action (${classification.reason}): ${action.type}`);
@@ -114,6 +214,10 @@ function assertActionAllowed(action, options = {}) {
114
214
  throw new Error(`Dangerous action requires explicit permission (${classification.reason}): ${action.type}`);
115
215
  }
116
216
 
217
+ if (classification.tier === TIERS.APPROVAL && !allowApproval) {
218
+ throw new Error(`Action requires approval (${classification.reason}): ${action.type}`);
219
+ }
220
+
117
221
  return classification;
118
222
  }
119
223
 
@@ -156,10 +260,14 @@ function appendActionLog(entry, options = {}) {
156
260
 
157
261
  module.exports = {
158
262
  TIERS,
263
+ getPolicy,
264
+ getAllowedRoots,
159
265
  classifyShellCommand,
160
266
  assertShellCommandAllowed,
161
267
  classifyAction,
162
268
  assertActionAllowed,
269
+ assertPathCapability,
270
+ resolveCapabilityPath,
163
271
  resolveWithinRoot,
164
272
  appendActionLog
165
273
  };
@@ -0,0 +1,182 @@
1
+ const { execFile, spawnSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const safetyManager = require('./safety_manager');
6
+
7
+ function commandExists(command) {
8
+ const lookup = process.platform === 'win32' ? 'where' : 'which';
9
+ const result = spawnSync(lookup, [command], { encoding: 'utf8', shell: false });
10
+ return result.status === 0;
11
+ }
12
+
13
+ function uniqueExistingRoots(roots) {
14
+ return Array.from(new Set((roots || [])
15
+ .filter(Boolean)
16
+ .map((root) => path.resolve(root))
17
+ .filter((root) => fs.existsSync(root))));
18
+ }
19
+
20
+ function buildBubblewrapArgs(command, options = {}) {
21
+ const cwd = path.resolve(options.cwd || process.cwd());
22
+ const readRoots = uniqueExistingRoots(safetyManager.getAllowedRoots('read'));
23
+ const writeRoots = uniqueExistingRoots(safetyManager.getAllowedRoots('write'));
24
+ const writableSet = new Set(writeRoots);
25
+ const bindRoots = uniqueExistingRoots([cwd, ...readRoots, ...writeRoots]);
26
+
27
+ const args = [
28
+ '--die-with-parent',
29
+ '--proc', '/proc',
30
+ '--dev', '/dev',
31
+ '--tmpfs', '/tmp',
32
+ '--ro-bind', '/usr', '/usr',
33
+ '--ro-bind', '/bin', '/bin',
34
+ '--ro-bind', '/etc', '/etc'
35
+ ];
36
+
37
+ for (const libPath of ['/lib', '/lib64']) {
38
+ if (fs.existsSync(libPath)) args.push('--ro-bind', libPath, libPath);
39
+ }
40
+
41
+ const parentDirs = new Set();
42
+ for (const root of bindRoots) {
43
+ let current = path.dirname(root);
44
+ while (current && current !== path.dirname(current)) {
45
+ parentDirs.add(current);
46
+ current = path.dirname(current);
47
+ }
48
+ }
49
+ for (const dir of Array.from(parentDirs).sort((a, b) => a.length - b.length)) {
50
+ if (!['/usr', '/bin', '/etc', '/lib', '/lib64'].includes(dir)) {
51
+ args.push('--dir', dir);
52
+ }
53
+ }
54
+
55
+ for (const root of bindRoots) {
56
+ const flag = writableSet.has(root) || root === cwd ? '--bind' : '--ro-bind';
57
+ args.push(flag, root, root);
58
+ }
59
+
60
+ args.push('--chdir', cwd, 'bash', '-lc', command);
61
+ return args;
62
+ }
63
+
64
+ function escapeSandboxProfileString(value) {
65
+ return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
66
+ }
67
+
68
+ function buildMacSandboxProfile(options = {}) {
69
+ const cwd = path.resolve(options.cwd || process.cwd());
70
+ const readRoots = uniqueExistingRoots(safetyManager.getAllowedRoots('read'));
71
+ const writeRoots = uniqueExistingRoots(safetyManager.getAllowedRoots('write'));
72
+ const allowedRead = uniqueExistingRoots([
73
+ cwd,
74
+ '/bin',
75
+ '/sbin',
76
+ '/usr',
77
+ '/System',
78
+ '/Library',
79
+ ...readRoots,
80
+ ...writeRoots
81
+ ]);
82
+ const allowedWrite = uniqueExistingRoots([cwd, ...writeRoots]);
83
+
84
+ const readRules = allowedRead.map((root) => ` (subpath "${escapeSandboxProfileString(root)}")`).join('\n');
85
+ const writeRules = allowedWrite.map((root) => ` (subpath "${escapeSandboxProfileString(root)}")`).join('\n');
86
+
87
+ return [
88
+ '(version 1)',
89
+ '(deny default)',
90
+ '(allow process*)',
91
+ '(allow sysctl-read)',
92
+ '(allow signal (target self))',
93
+ '(allow file-read-metadata)',
94
+ '(allow file-read*',
95
+ readRules,
96
+ ')',
97
+ '(allow file-write*',
98
+ writeRules,
99
+ ` (subpath "${escapeSandboxProfileString(os.tmpdir())}")`,
100
+ ')'
101
+ ].join('\n');
102
+ }
103
+
104
+ function getShellInvocation(command) {
105
+ if (process.platform === 'win32') {
106
+ return {
107
+ command: 'powershell.exe',
108
+ args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command]
109
+ };
110
+ }
111
+ return {
112
+ command: 'bash',
113
+ args: ['-lc', command]
114
+ };
115
+ }
116
+
117
+ function execFilePromise(command, args, options = {}) {
118
+ return new Promise((resolve, reject) => {
119
+ execFile(command, args, options, (error, stdout, stderr) => {
120
+ if (error) {
121
+ error.stdout = stdout;
122
+ error.stderr = stderr;
123
+ reject(error);
124
+ return;
125
+ }
126
+ resolve({ stdout, stderr });
127
+ });
128
+ });
129
+ }
130
+
131
+ async function runShell(command, options = {}) {
132
+ const policy = safetyManager.getPolicy();
133
+ safetyManager.assertShellCommandAllowed(command);
134
+
135
+ const cwd = path.resolve(options.cwd || process.cwd());
136
+ const execOptions = {
137
+ cwd,
138
+ maxBuffer: options.maxBuffer || 1024 * 1024 * 4,
139
+ env: options.env || process.env
140
+ };
141
+
142
+ if (!policy.enabled || policy.sandboxMode === 'off') {
143
+ const shell = getShellInvocation(command);
144
+ return execFilePromise(shell.command, shell.args, execOptions);
145
+ }
146
+
147
+ const sandboxCommand = policy.sandboxCommand || 'bwrap';
148
+ if (process.platform === 'linux' && commandExists(sandboxCommand)) {
149
+ return execFilePromise(sandboxCommand, buildBubblewrapArgs(command, { cwd }), execOptions);
150
+ }
151
+
152
+ if (process.platform === 'darwin' && commandExists('sandbox-exec')) {
153
+ return execFilePromise('sandbox-exec', ['-p', buildMacSandboxProfile({ cwd }), 'bash', '-lc', command], execOptions);
154
+ }
155
+
156
+ if (policy.sandboxMode === 'enforce') {
157
+ const hint = process.platform === 'darwin'
158
+ ? "macOS sandbox-exec is not available."
159
+ : process.platform === 'win32'
160
+ ? 'Windows sandbox provider is not configured. Use WSL/containers or set sandboxMode to prefer.'
161
+ : `Sandbox command '${sandboxCommand}' is not available.`;
162
+ throw new Error(`Sandbox is enforced but no sandbox provider could run. ${hint}`);
163
+ }
164
+
165
+ safetyManager.appendActionLog({
166
+ source: options.source || 'sandbox_runner',
167
+ action: 'sandbox_fallback',
168
+ sandboxCommand,
169
+ platform: process.platform,
170
+ cwd
171
+ });
172
+ const shell = getShellInvocation(command);
173
+ return execFilePromise(shell.command, shell.args, execOptions);
174
+ }
175
+
176
+ module.exports = {
177
+ runShell,
178
+ buildBubblewrapArgs,
179
+ buildMacSandboxProfile,
180
+ getShellInvocation,
181
+ commandExists
182
+ };