@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.
- package/README.md +35 -1
- package/main.js +28 -14
- package/mint-cli-logic.js +3 -119
- package/mint-cli.js +201 -500
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +40 -0
- 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
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +15 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
- 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
- package/package.json +40 -17
- package/src/AI_Brain/Gemini_API.js +147 -46
- package/src/AI_Brain/autonomous_brain.js +2 -1
- package/src/AI_Brain/memory_store.js +299 -3
- package/src/AI_Brain/proactive_engine.js +12 -2
- package/src/Automation_Layer/browser_automation.js +26 -24
- package/src/CLI/approval_handler.js +42 -0
- package/src/CLI/chat_router.js +18 -6
- package/src/CLI/chat_ui.js +583 -52
- package/src/CLI/cli_colors.js +32 -0
- package/src/CLI/cli_formatters.js +89 -0
- package/src/CLI/code_agent.js +369 -71
- package/src/CLI/image_input.js +90 -0
- package/src/CLI/intent_detectors.js +181 -0
- package/src/CLI/interactive_chat.js +479 -0
- package/src/CLI/list_features.js +3 -0
- package/src/CLI/onboarding.js +72 -15
- package/src/CLI/repo_summarizer.js +282 -0
- package/src/CLI/semantic_code_search.js +312 -0
- package/src/CLI/skill_manager.js +41 -0
- package/src/CLI/slash_command_handler.js +418 -0
- package/src/CLI/symbol_indexer.js +231 -0
- package/src/CLI/updater.js +6 -4
- package/src/Channels/discord_bridge.js +11 -13
- package/src/Channels/line_bridge.js +10 -10
- package/src/Channels/slack_bridge.js +7 -12
- package/src/Channels/telegram_bridge.js +6 -14
- package/src/Channels/whatsapp_bridge.js +11 -9
- package/src/System/action_executor.js +59 -10
- package/src/System/chat_history_manager.js +20 -12
- package/src/System/config_manager.js +31 -1
- package/src/System/granular_automation.js +122 -53
- package/src/System/optional_require.js +23 -0
- package/src/System/proactive_loop.js +19 -3
- package/src/System/safety_manager.js +108 -0
- package/src/System/sandbox_runner.js +182 -0
- package/src/System/system_automation.js +127 -81
- package/src/System/system_info.js +70 -0
- package/src/System/tool_registry.js +280 -0
- package/src/System/window_manager.js +4 -2
- package/src/UI/live2d_manager.js +566 -0
- package/src/UI/renderer.js +339 -21
- package/src/UI/settings.css +655 -420
- package/src/UI/settings.html +478 -432
- package/src/UI/settings.js +10 -8
- package/src/UI/styles.css +516 -31
- package/.codex +0 -0
- package/docs/assets/Agent_Mint.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/assets/Settings.png +0 -0
- package/docs/assets/icon.png +0 -0
- package/docs/guide.html +0 -632
- package/docs/index.html +0 -133
- package/docs/style.css +0 -579
- package/index.html +0 -16
- package/src/UI/index.html +0 -126
- package/tech_news.txt +0 -3
- package/test_knowledge.txt +0 -3
- package/tests/action_executor_safety.test.js +0 -67
- package/tests/agent_orchestrator.test.js +0 -41
- package/tests/chat_router.test.js +0 -42
- package/tests/code_agent.test.js +0 -69
- package/tests/config_manager.test.js +0 -141
- package/tests/docker.test.js +0 -46
- package/tests/file_operations.test.js +0 -57
- package/tests/gmail.test.js +0 -135
- package/tests/gmail_auth.test.js +0 -129
- package/tests/google_calendar.test.js +0 -113
- package/tests/google_tts_urls.test.js +0 -24
- package/tests/memory_store.test.js +0 -185
- package/tests/notion.test.js +0 -121
- package/tests/provider_routing.test.js +0 -83
- package/tests/safety_manager.test.js +0 -40
- package/tests/spotify.test.js +0 -201
- package/tests/system_monitor.test.js +0 -37
- package/tests/updater.test.js +0 -32
- package/tests/workspace_manager.test.js +0 -56
|
@@ -1,88 +1,157 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { execFile, spawnSync } = require('child_process');
|
|
2
2
|
const { screen } = require('electron');
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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;
|
|
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 (
|
|
25
|
-
//
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
return this.run(`xdotool key "${key}"`);
|
|
91
|
+
keyTap(key) {
|
|
92
|
+
return this.provider().keyTap(String(key || ''));
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
|
|
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
|
+
};
|