shoud-cli 3.0.1 → 3.0.3
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/bin/shoud.js +1 -1
- package/package.json +1 -1
- package/src/context/checkpoint.js +0 -82
- package/src/permissions/engine.js +0 -133
package/bin/shoud.js
CHANGED
|
@@ -28,7 +28,7 @@ async function main() {
|
|
|
28
28
|
|
|
29
29
|
program
|
|
30
30
|
.name('shoud')
|
|
31
|
-
.version(
|
|
31
|
+
.version(pkg.version)
|
|
32
32
|
.description('SHOUD Terminal Agent')
|
|
33
33
|
.argument('[prompt...]', 'The task you want SHOUD to execute')
|
|
34
34
|
.option('--budget <amount>', 'Budget for this job in USD', parseFloat)
|
package/package.json
CHANGED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
|
|
5
|
-
const CONFIG_DIR = path.join(os.homedir(), '.shoud');
|
|
6
|
-
const CHECKPOINT_FILE = path.join(CONFIG_DIR, 'checkpoint.json');
|
|
7
|
-
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
8
|
-
|
|
9
|
-
// Ensure config directory exists with secure permissions
|
|
10
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
11
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Save the execution state (messages, etc.) to a checkpoint file.
|
|
16
|
-
* Includes a timestamp for TTL.
|
|
17
|
-
*/
|
|
18
|
-
function saveCheckpoint(state) {
|
|
19
|
-
try {
|
|
20
|
-
const data = {
|
|
21
|
-
timestamp: Date.now(),
|
|
22
|
-
...state
|
|
23
|
-
};
|
|
24
|
-
fs.writeFileSync(CHECKPOINT_FILE, JSON.stringify(data, null, 2), {
|
|
25
|
-
encoding: 'utf-8',
|
|
26
|
-
mode: 0o600
|
|
27
|
-
});
|
|
28
|
-
} catch (err) {
|
|
29
|
-
console.error(`Failed to save checkpoint: ${err.message}`);
|
|
30
|
-
// We don't throw; the agent can continue without checkpointing
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Load a checkpoint if it exists and is not too old.
|
|
36
|
-
* Returns the state object (e.g., { messages }) or null if none or expired.
|
|
37
|
-
*/
|
|
38
|
-
function loadCheckpoint() {
|
|
39
|
-
if (!fs.existsSync(CHECKPOINT_FILE)) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
const raw = fs.readFileSync(CHECKPOINT_FILE, 'utf-8');
|
|
44
|
-
const data = JSON.parse(raw);
|
|
45
|
-
const { timestamp, ...state } = data;
|
|
46
|
-
|
|
47
|
-
// Check TTL
|
|
48
|
-
if (timestamp && (Date.now() - timestamp) > MAX_AGE_MS) {
|
|
49
|
-
// Stale checkpoint – delete it and ignore
|
|
50
|
-
clearCheckpoint();
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Validate that the state contains at least a messages array
|
|
55
|
-
if (!state.messages || !Array.isArray(state.messages)) {
|
|
56
|
-
// Corrupted or invalid state; delete it
|
|
57
|
-
clearCheckpoint();
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return state;
|
|
62
|
-
} catch (err) {
|
|
63
|
-
// If file is corrupt, delete it
|
|
64
|
-
clearCheckpoint();
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Clear (delete) the checkpoint file.
|
|
71
|
-
*/
|
|
72
|
-
function clearCheckpoint() {
|
|
73
|
-
if (fs.existsSync(CHECKPOINT_FILE)) {
|
|
74
|
-
try {
|
|
75
|
-
fs.unlinkSync(CHECKPOINT_FILE);
|
|
76
|
-
} catch (_) {
|
|
77
|
-
// Ignore errors during cleanup
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
module.exports = { saveCheckpoint, loadCheckpoint, clearCheckpoint };
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
const inquirer = require('inquirer');
|
|
2
|
-
const chalk = require('chalk');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
|
|
7
|
-
const CONFIG_DIR = path.join(os.homedir(), '.shoud');
|
|
8
|
-
const ALLOWLIST_FILE = path.join(CONFIG_DIR, 'allowlist.json');
|
|
9
|
-
|
|
10
|
-
// Ensure config directory exists
|
|
11
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
12
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Load persistent allowlist (or create empty)
|
|
16
|
-
function loadAllowlist() {
|
|
17
|
-
try {
|
|
18
|
-
if (fs.existsSync(ALLOWLIST_FILE)) {
|
|
19
|
-
return JSON.parse(fs.readFileSync(ALLOWLIST_FILE, 'utf-8'));
|
|
20
|
-
}
|
|
21
|
-
} catch (_) {}
|
|
22
|
-
return { commands: [], paths: [] };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Save allowlist
|
|
26
|
-
function saveAllowlist(data) {
|
|
27
|
-
try {
|
|
28
|
-
fs.writeFileSync(ALLOWLIST_FILE, JSON.stringify(data, null, 2), {
|
|
29
|
-
encoding: 'utf-8',
|
|
30
|
-
mode: 0o600
|
|
31
|
-
});
|
|
32
|
-
} catch (_) {}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// In‑memory session allowlist (cleared on CLI restart)
|
|
36
|
-
let sessionAllowList = {
|
|
37
|
-
commands: new Set(),
|
|
38
|
-
paths: new Set()
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Check if a command is allowed by persistent or session allowlists.
|
|
43
|
-
*/
|
|
44
|
-
function isAllowed(toolName, input) {
|
|
45
|
-
if (toolName === 'read_file') return true; // always safe
|
|
46
|
-
|
|
47
|
-
if (toolName === 'execute_shell') {
|
|
48
|
-
const cmd = input.command?.trim().split(/\s+/)[0] || '';
|
|
49
|
-
// Persistent allowlist
|
|
50
|
-
const persistent = loadAllowlist();
|
|
51
|
-
if (persistent.commands.includes(cmd)) return true;
|
|
52
|
-
// Session allowlist
|
|
53
|
-
if (sessionAllowList.commands.has(cmd)) return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (toolName === 'write_file' || toolName === 'read_file') {
|
|
57
|
-
const filePath = input.path || '';
|
|
58
|
-
// Check persistent and session for paths (exact match or prefix)
|
|
59
|
-
// For simplicity, we'll just match exact paths; could be extended
|
|
60
|
-
const persistent = loadAllowlist();
|
|
61
|
-
if (persistent.paths.includes(filePath)) return true;
|
|
62
|
-
if (sessionAllowList.paths.has(filePath)) return true;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Prompt user for permission, with options for session‑wide or persistent allow.
|
|
70
|
-
*/
|
|
71
|
-
async function verifyPermission(toolName, input) {
|
|
72
|
-
// If already allowed, skip prompt
|
|
73
|
-
if (isAllowed(toolName, input)) return true;
|
|
74
|
-
|
|
75
|
-
// Build display info
|
|
76
|
-
let target = '';
|
|
77
|
-
if (toolName === 'execute_shell') target = input.command || 'unknown command';
|
|
78
|
-
else if (toolName === 'read_file' || toolName === 'write_file') target = input.path || 'unknown path';
|
|
79
|
-
else target = JSON.stringify(input);
|
|
80
|
-
|
|
81
|
-
console.log(chalk.yellow(`\n⚠ SHOUD requests permission to run: ${chalk.bold(toolName)}`));
|
|
82
|
-
console.log(chalk.gray(`Target: ${target}`));
|
|
83
|
-
|
|
84
|
-
const { permission } = await inquirer.prompt([
|
|
85
|
-
{
|
|
86
|
-
type: 'list',
|
|
87
|
-
name: 'permission',
|
|
88
|
-
message: 'Allow this operation?',
|
|
89
|
-
choices: [
|
|
90
|
-
{ name: 'Allow once', value: 'once' },
|
|
91
|
-
{ name: 'Allow for this session', value: 'session' },
|
|
92
|
-
{ name: 'Always allow (save to config)', value: 'always' },
|
|
93
|
-
{ name: 'Deny', value: 'deny' }
|
|
94
|
-
]
|
|
95
|
-
}
|
|
96
|
-
]);
|
|
97
|
-
|
|
98
|
-
if (permission === 'deny') return false;
|
|
99
|
-
|
|
100
|
-
// Add to session allowlist if requested
|
|
101
|
-
if (permission === 'session') {
|
|
102
|
-
if (toolName === 'execute_shell') {
|
|
103
|
-
const cmd = input.command.trim().split(/\s+/)[0];
|
|
104
|
-
sessionAllowList.commands.add(cmd);
|
|
105
|
-
} else if (toolName === 'read_file' || toolName === 'write_file') {
|
|
106
|
-
sessionAllowList.paths.add(input.path);
|
|
107
|
-
}
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Add to persistent allowlist
|
|
112
|
-
if (permission === 'always') {
|
|
113
|
-
const allowlist = loadAllowlist();
|
|
114
|
-
if (toolName === 'execute_shell') {
|
|
115
|
-
const cmd = input.command.trim().split(/\s+/)[0];
|
|
116
|
-
if (!allowlist.commands.includes(cmd)) {
|
|
117
|
-
allowlist.commands.push(cmd);
|
|
118
|
-
saveAllowlist(allowlist);
|
|
119
|
-
}
|
|
120
|
-
} else if (toolName === 'read_file' || toolName === 'write_file') {
|
|
121
|
-
if (!allowlist.paths.includes(input.path)) {
|
|
122
|
-
allowlist.paths.push(input.path);
|
|
123
|
-
saveAllowlist(allowlist);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Once
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
module.exports = { verifyPermission };
|