wispy-cli 0.9.0 → 1.1.0
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/wispy.mjs +293 -0
- package/core/audit.mjs +322 -0
- package/core/cron.mjs +28 -16
- package/core/engine.mjs +237 -12
- package/core/index.mjs +4 -0
- package/core/nodes.mjs +228 -0
- package/core/permissions.mjs +248 -0
- package/core/server.mjs +522 -0
- package/core/session.mjs +13 -1
- package/core/subagents.mjs +13 -3
- package/lib/wispy-repl.mjs +82 -0
- package/package.json +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/permissions.mjs — Permission & Approval System for Wispy
|
|
3
|
+
*
|
|
4
|
+
* Policy levels:
|
|
5
|
+
* "auto" — execute silently
|
|
6
|
+
* "notify" — execute but log to audit trail
|
|
7
|
+
* "approve" — pause and ask user before executing
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
13
|
+
|
|
14
|
+
import { WISPY_DIR } from "./config.mjs";
|
|
15
|
+
|
|
16
|
+
// ── Default policies ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_POLICIES = {
|
|
19
|
+
// Auto: safe read-only or low-impact
|
|
20
|
+
read_file: "auto",
|
|
21
|
+
list_directory: "auto",
|
|
22
|
+
web_search: "auto",
|
|
23
|
+
web_fetch: "auto",
|
|
24
|
+
memory_search: "auto",
|
|
25
|
+
memory_list: "auto",
|
|
26
|
+
memory_get: "auto",
|
|
27
|
+
kill_subagent: "auto",
|
|
28
|
+
list_agents: "auto",
|
|
29
|
+
list_subagents: "auto",
|
|
30
|
+
get_agent_result: "auto",
|
|
31
|
+
get_subagent_result: "auto",
|
|
32
|
+
|
|
33
|
+
// Notify: medium-risk writes
|
|
34
|
+
write_file: "notify",
|
|
35
|
+
file_edit: "notify",
|
|
36
|
+
memory_save: "notify",
|
|
37
|
+
memory_append: "notify",
|
|
38
|
+
memory_delete: "notify",
|
|
39
|
+
clipboard: "notify",
|
|
40
|
+
spawn_subagent: "notify",
|
|
41
|
+
spawn_agent: "notify",
|
|
42
|
+
spawn_async_agent: "notify",
|
|
43
|
+
|
|
44
|
+
// Approve: high-risk operations
|
|
45
|
+
run_command: "approve",
|
|
46
|
+
git: "approve",
|
|
47
|
+
keychain: "approve",
|
|
48
|
+
delete_file: "approve",
|
|
49
|
+
pipeline: "notify",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ── Scope definitions ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export const BUILT_IN_SCOPES = {
|
|
55
|
+
"files:read": ["read_file", "list_directory"],
|
|
56
|
+
"files:write": ["write_file", "file_edit", "delete_file"],
|
|
57
|
+
"web": ["web_search", "web_fetch"],
|
|
58
|
+
"memory:read": ["memory_search", "memory_list", "memory_get"],
|
|
59
|
+
"memory:write": ["memory_save", "memory_append", "memory_delete"],
|
|
60
|
+
"commands:execute": ["run_command"],
|
|
61
|
+
"agents": ["spawn_subagent", "spawn_agent", "spawn_async_agent", "list_subagents", "list_agents"],
|
|
62
|
+
"system": ["git", "keychain", "clipboard"],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const PERMISSIONS_FILE = path.join(WISPY_DIR, "permissions.json");
|
|
66
|
+
|
|
67
|
+
export class PermissionManager {
|
|
68
|
+
constructor(config = {}) {
|
|
69
|
+
this.config = config;
|
|
70
|
+
this._policies = { ...DEFAULT_POLICIES };
|
|
71
|
+
this._scopes = new Set();
|
|
72
|
+
this._loaded = false;
|
|
73
|
+
|
|
74
|
+
// For REPL approval prompts — set by REPL
|
|
75
|
+
this._approvalHandler = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load persisted policy overrides from disk.
|
|
80
|
+
*/
|
|
81
|
+
async load() {
|
|
82
|
+
if (this._loaded) return;
|
|
83
|
+
try {
|
|
84
|
+
const raw = await readFile(PERMISSIONS_FILE, "utf8");
|
|
85
|
+
const data = JSON.parse(raw);
|
|
86
|
+
if (data.policies) Object.assign(this._policies, data.policies);
|
|
87
|
+
if (data.scopes) this._scopes = new Set(data.scopes);
|
|
88
|
+
} catch {
|
|
89
|
+
// First run — no file yet
|
|
90
|
+
}
|
|
91
|
+
this._loaded = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Persist current policy overrides to disk.
|
|
96
|
+
*/
|
|
97
|
+
async save() {
|
|
98
|
+
await mkdir(WISPY_DIR, { recursive: true });
|
|
99
|
+
const overrides = {};
|
|
100
|
+
for (const [k, v] of Object.entries(this._policies)) {
|
|
101
|
+
if (DEFAULT_POLICIES[k] !== v) overrides[k] = v;
|
|
102
|
+
}
|
|
103
|
+
await writeFile(PERMISSIONS_FILE, JSON.stringify({
|
|
104
|
+
policies: overrides,
|
|
105
|
+
scopes: [...this._scopes],
|
|
106
|
+
}, null, 2) + "\n", "utf8");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Policy accessors ────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
getPolicy(toolName) {
|
|
112
|
+
return this._policies[toolName] ?? "auto";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setPolicy(toolName, level) {
|
|
116
|
+
if (!["auto", "notify", "approve"].includes(level)) {
|
|
117
|
+
throw new Error(`Invalid policy level: ${level}. Must be auto, notify, or approve.`);
|
|
118
|
+
}
|
|
119
|
+
this._policies[toolName] = level;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
listPolicies() {
|
|
123
|
+
return { ...this._policies };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Scope management ────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
addScope(scope) {
|
|
129
|
+
this._scopes.add(scope);
|
|
130
|
+
// Apply the scope's tools
|
|
131
|
+
if (BUILT_IN_SCOPES[scope]) {
|
|
132
|
+
for (const tool of BUILT_IN_SCOPES[scope]) {
|
|
133
|
+
this._policies[tool] = "auto";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
removeScope(scope) {
|
|
139
|
+
this._scopes.delete(scope);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
listScopes() {
|
|
143
|
+
return [...this._scopes];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Check ───────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if a tool call is allowed.
|
|
150
|
+
* @returns {{ allowed: boolean, level: string, needsApproval?: boolean, reason?: string }}
|
|
151
|
+
*/
|
|
152
|
+
async check(toolName, args, context = {}) {
|
|
153
|
+
await this.load();
|
|
154
|
+
const level = this.getPolicy(toolName);
|
|
155
|
+
|
|
156
|
+
if (level === "auto") {
|
|
157
|
+
return { allowed: true, level: "auto" };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (level === "notify") {
|
|
161
|
+
// Allowed, but should be logged
|
|
162
|
+
return { allowed: true, level: "notify" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (level === "approve") {
|
|
166
|
+
// Need explicit approval
|
|
167
|
+
const approved = await this.requestApproval({
|
|
168
|
+
toolName,
|
|
169
|
+
args,
|
|
170
|
+
context,
|
|
171
|
+
label: this._formatAction(toolName, args),
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
allowed: approved,
|
|
175
|
+
level: "approve",
|
|
176
|
+
needsApproval: true,
|
|
177
|
+
approved,
|
|
178
|
+
reason: approved ? "User approved" : "User denied",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { allowed: true, level };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Request user approval for an action.
|
|
187
|
+
* @param {{ toolName, args, label, context }} action
|
|
188
|
+
* @returns {boolean} approved
|
|
189
|
+
*/
|
|
190
|
+
async requestApproval(action) {
|
|
191
|
+
if (this._approvalHandler) {
|
|
192
|
+
return this._approvalHandler(action);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Default: CLI readline prompt
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
198
|
+
const prompt = `\n⚠️ ${action.label ?? `${action.toolName}()`} — Allow? [y/N] `;
|
|
199
|
+
rl.question(prompt, (answer) => {
|
|
200
|
+
rl.close();
|
|
201
|
+
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Set a custom approval handler (for channels, testing, etc.)
|
|
208
|
+
* @param {Function} fn — async (action) => boolean
|
|
209
|
+
*/
|
|
210
|
+
setApprovalHandler(fn) {
|
|
211
|
+
this._approvalHandler = fn;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
_formatAction(toolName, args) {
|
|
217
|
+
let argsStr = "";
|
|
218
|
+
try {
|
|
219
|
+
argsStr = JSON.stringify(args);
|
|
220
|
+
if (argsStr.length > 80) argsStr = argsStr.slice(0, 77) + "...";
|
|
221
|
+
} catch {}
|
|
222
|
+
return `${toolName}(${argsStr})`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Format policy table for display.
|
|
227
|
+
*/
|
|
228
|
+
formatTable() {
|
|
229
|
+
const lines = [];
|
|
230
|
+
const all = { ...DEFAULT_POLICIES, ...this._policies };
|
|
231
|
+
const grouped = { approve: [], notify: [], auto: [] };
|
|
232
|
+
for (const [tool, level] of Object.entries(all)) {
|
|
233
|
+
grouped[level]?.push(tool);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
lines.push("Permission Policies:\n");
|
|
237
|
+
const icons = { approve: "🔐", notify: "📋", auto: "✅" };
|
|
238
|
+
for (const level of ["approve", "notify", "auto"]) {
|
|
239
|
+
if (grouped[level].length > 0) {
|
|
240
|
+
lines.push(` ${icons[level]} ${level.toUpperCase()}:`);
|
|
241
|
+
lines.push(` ${grouped[level].join(", ")}`);
|
|
242
|
+
lines.push("");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
}
|