openclaw-plugin-exec-grant 1.1.0 → 1.2.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/index.ts +258 -10
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
3
5
|
|
|
4
6
|
const PLUGIN_ID = "openclaw-plugin-exec-grant";
|
|
5
7
|
const SCRIPTS_DIR = join(__dirname, "skills", "exec-grant", "scripts");
|
|
8
|
+
const APPROVALS_FILE = join(homedir(), ".openclaw", "exec-approvals.json");
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ALLOWLIST = [
|
|
11
|
+
"ls",
|
|
12
|
+
"cat",
|
|
13
|
+
"head",
|
|
14
|
+
"tail",
|
|
15
|
+
"wc",
|
|
16
|
+
"grep",
|
|
17
|
+
"find",
|
|
18
|
+
"file",
|
|
19
|
+
"echo",
|
|
20
|
+
"printf",
|
|
21
|
+
"date",
|
|
22
|
+
"whoami",
|
|
23
|
+
"pwd",
|
|
24
|
+
"git status",
|
|
25
|
+
"git log",
|
|
26
|
+
"git diff",
|
|
27
|
+
"git show",
|
|
28
|
+
"node --version",
|
|
29
|
+
"npm --version",
|
|
30
|
+
"python3 --version",
|
|
31
|
+
"openclaw config get",
|
|
32
|
+
"openclaw message send",
|
|
33
|
+
"jq",
|
|
34
|
+
"sed",
|
|
35
|
+
"awk",
|
|
36
|
+
"sort",
|
|
37
|
+
"uniq",
|
|
38
|
+
"tr",
|
|
39
|
+
"cut",
|
|
40
|
+
];
|
|
6
41
|
|
|
7
42
|
function runScript(
|
|
8
43
|
name: string,
|
|
@@ -23,20 +58,190 @@ function runScript(
|
|
|
23
58
|
}
|
|
24
59
|
}
|
|
25
60
|
|
|
61
|
+
function ensureAllowlistBaseline(logger: any): void {
|
|
62
|
+
mkdirSync(dirname(APPROVALS_FILE), { recursive: true });
|
|
63
|
+
|
|
64
|
+
if (!existsSync(APPROVALS_FILE)) {
|
|
65
|
+
const baseline = {
|
|
66
|
+
agents: {
|
|
67
|
+
main: {
|
|
68
|
+
security: "allowlist",
|
|
69
|
+
allowlist: DEFAULT_ALLOWLIST,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
writeFileSync(APPROVALS_FILE, JSON.stringify(baseline, null, 2) + "\n");
|
|
74
|
+
logger?.info(
|
|
75
|
+
`[exec-grant] Created ${APPROVALS_FILE} with allowlist baseline (${DEFAULT_ALLOWLIST.length} commands)`,
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// File exists -- check if security mode is set
|
|
81
|
+
try {
|
|
82
|
+
const data = JSON.parse(readFileSync(APPROVALS_FILE, "utf-8"));
|
|
83
|
+
const security = data?.agents?.main?.security;
|
|
84
|
+
if (!security) {
|
|
85
|
+
// Set security to allowlist without overwriting other fields
|
|
86
|
+
if (!data.agents) data.agents = {};
|
|
87
|
+
if (!data.agents.main) data.agents.main = {};
|
|
88
|
+
data.agents.main.security = "allowlist";
|
|
89
|
+
if (!data.agents.main.allowlist) {
|
|
90
|
+
data.agents.main.allowlist = DEFAULT_ALLOWLIST;
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(APPROVALS_FILE, JSON.stringify(data, null, 2) + "\n");
|
|
93
|
+
logger?.info(
|
|
94
|
+
`[exec-grant] Set agents.main.security = "allowlist" in ${APPROVALS_FILE}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// File exists but isn't valid JSON -- leave it alone
|
|
99
|
+
logger?.warn(`[exec-grant] Could not parse ${APPROVALS_FILE}, skipping baseline setup`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
26
103
|
export default function register(api: any) {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
104
|
+
const logger = api.logger;
|
|
105
|
+
const pluginConfig = api.pluginConfig ?? {};
|
|
106
|
+
|
|
107
|
+
// --- Auto-create allowlist baseline ---
|
|
108
|
+
ensureAllowlistBaseline(logger);
|
|
109
|
+
|
|
110
|
+
// --- Startup guidance ---
|
|
111
|
+
const adminTarget = pluginConfig.adminTarget;
|
|
112
|
+
const channel = pluginConfig.channel;
|
|
30
113
|
|
|
31
114
|
if (!adminTarget || !channel) {
|
|
32
|
-
|
|
33
|
-
`[exec-grant]
|
|
34
|
-
` openclaw
|
|
35
|
-
`
|
|
36
|
-
`
|
|
115
|
+
logger?.warn(
|
|
116
|
+
`[exec-grant] Plugin installed but not configured yet.\n` +
|
|
117
|
+
` Run: openclaw exec-grant-setup\n` +
|
|
118
|
+
` Or manually set in openclaw.json:\n` +
|
|
119
|
+
` plugins.entries.${PLUGIN_ID}.config.adminTarget = "<target>"\n` +
|
|
120
|
+
` plugins.entries.${PLUGIN_ID}.config.channel = "<channel>"`,
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
logger?.info(
|
|
124
|
+
`[exec-grant] Ready. Admin: ${adminTarget} via ${channel}. ` +
|
|
125
|
+
`Commands: /grant <min>, /revoke, /grant-status`,
|
|
37
126
|
);
|
|
38
127
|
}
|
|
39
128
|
|
|
129
|
+
// --- Interactive setup CLI command ---
|
|
130
|
+
api.registerCli(
|
|
131
|
+
({ program, config }: any) => {
|
|
132
|
+
program
|
|
133
|
+
.command("exec-grant-setup")
|
|
134
|
+
.description("Configure the exec-grant plugin interactively")
|
|
135
|
+
.action(async () => {
|
|
136
|
+
// Dynamic import for prompts (clack is bundled with openclaw)
|
|
137
|
+
let prompts: any;
|
|
138
|
+
try {
|
|
139
|
+
prompts = await import("@clack/prompts");
|
|
140
|
+
} catch {
|
|
141
|
+
// Fallback: try readline
|
|
142
|
+
console.log("\n exec-grant setup\n");
|
|
143
|
+
console.log(" Could not load interactive prompts.");
|
|
144
|
+
console.log(" Configure manually in openclaw.json:\n");
|
|
145
|
+
printManualConfig();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
prompts.intro("exec-grant setup");
|
|
150
|
+
|
|
151
|
+
const currentConfig =
|
|
152
|
+
config?.plugins?.entries?.[PLUGIN_ID]?.config ?? {};
|
|
153
|
+
|
|
154
|
+
const channelChoice = await prompts.select({
|
|
155
|
+
message: "Which messaging channel should approval requests use?",
|
|
156
|
+
options: [
|
|
157
|
+
{ value: "whatsapp", label: "WhatsApp" },
|
|
158
|
+
{ value: "telegram", label: "Telegram" },
|
|
159
|
+
{ value: "slack", label: "Slack" },
|
|
160
|
+
{ value: "discord", label: "Discord" },
|
|
161
|
+
{ value: "signal", label: "Signal" },
|
|
162
|
+
{ value: "msteams", label: "Microsoft Teams" },
|
|
163
|
+
{ value: "matrix", label: "Matrix" },
|
|
164
|
+
{ value: "other", label: "Other" },
|
|
165
|
+
],
|
|
166
|
+
initialValue: currentConfig.channel || "whatsapp",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (prompts.isCancel(channelChoice)) {
|
|
170
|
+
prompts.cancel("Setup cancelled.");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let selectedChannel = channelChoice as string;
|
|
175
|
+
if (selectedChannel === "other") {
|
|
176
|
+
const custom = await prompts.text({
|
|
177
|
+
message: "Enter the channel name:",
|
|
178
|
+
placeholder: "nostr",
|
|
179
|
+
});
|
|
180
|
+
if (prompts.isCancel(custom)) {
|
|
181
|
+
prompts.cancel("Setup cancelled.");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
selectedChannel = custom as string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const targetHint = getTargetHint(selectedChannel);
|
|
188
|
+
const target = await prompts.text({
|
|
189
|
+
message: `Admin contact (${targetHint.label}):`,
|
|
190
|
+
placeholder: targetHint.placeholder,
|
|
191
|
+
initialValue: currentConfig.adminTarget || "",
|
|
192
|
+
validate: (val: string) =>
|
|
193
|
+
val.trim().length === 0 ? "Required" : undefined,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (prompts.isCancel(target)) {
|
|
197
|
+
prompts.cancel("Setup cancelled.");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Write config
|
|
202
|
+
console.log("");
|
|
203
|
+
try {
|
|
204
|
+
execFileSync("bash", [
|
|
205
|
+
"-c",
|
|
206
|
+
`openclaw config set "plugins.entries.${PLUGIN_ID}.config.channel" "${selectedChannel}" && ` +
|
|
207
|
+
`openclaw config set "plugins.entries.${PLUGIN_ID}.config.adminTarget" "${(target as string).trim()}"`,
|
|
208
|
+
], { encoding: "utf-8", timeout: 15_000 });
|
|
209
|
+
} catch {
|
|
210
|
+
prompts.log.error("Failed to write config. Set manually:");
|
|
211
|
+
printManualConfig(selectedChannel, (target as string).trim());
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
prompts.log.success(
|
|
216
|
+
`Channel: ${selectedChannel}\n Admin: ${(target as string).trim()}`,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Check allowlist baseline
|
|
220
|
+
if (existsSync(APPROVALS_FILE)) {
|
|
221
|
+
try {
|
|
222
|
+
const data = JSON.parse(readFileSync(APPROVALS_FILE, "utf-8"));
|
|
223
|
+
const sec = data?.agents?.main?.security;
|
|
224
|
+
prompts.log.info(
|
|
225
|
+
`Allowlist baseline: ${APPROVALS_FILE}\n Security mode: ${sec || "not set"}`,
|
|
226
|
+
);
|
|
227
|
+
} catch {
|
|
228
|
+
// skip
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
prompts.log.info(
|
|
233
|
+
"Restart the gateway to apply changes:\n openclaw gateway restart",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
prompts.outro(
|
|
237
|
+
"Setup complete! Commands available: /grant <min>, /revoke, /grant-status",
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
{ commands: ["exec-grant-setup"] },
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// --- Slash commands ---
|
|
40
245
|
api.registerCommand({
|
|
41
246
|
name: "grant",
|
|
42
247
|
description: "Activate time-boxed elevated shell access (admin only)",
|
|
@@ -47,7 +252,6 @@ export default function register(api: any) {
|
|
|
47
252
|
if (!minutes || !/^\d+$/.test(minutes)) {
|
|
48
253
|
return { text: "Usage: /grant <minutes> (1-120)" };
|
|
49
254
|
}
|
|
50
|
-
// Reply on the channel the admin used to send the command
|
|
51
255
|
const replyChannel = ctx.channel || channel;
|
|
52
256
|
const replyTarget = ctx.sender || adminTarget;
|
|
53
257
|
const result = runScript("grant.sh", [minutes], {
|
|
@@ -85,3 +289,47 @@ export default function register(api: any) {
|
|
|
85
289
|
},
|
|
86
290
|
});
|
|
87
291
|
}
|
|
292
|
+
|
|
293
|
+
function getTargetHint(channel: string): {
|
|
294
|
+
label: string;
|
|
295
|
+
placeholder: string;
|
|
296
|
+
} {
|
|
297
|
+
switch (channel) {
|
|
298
|
+
case "whatsapp":
|
|
299
|
+
case "signal":
|
|
300
|
+
return { label: "E.164 phone number", placeholder: "+16505551234" };
|
|
301
|
+
case "telegram":
|
|
302
|
+
return { label: "chat ID or @username", placeholder: "@admin" };
|
|
303
|
+
case "slack":
|
|
304
|
+
return {
|
|
305
|
+
label: "channel or user ID",
|
|
306
|
+
placeholder: "#approvals or @admin",
|
|
307
|
+
};
|
|
308
|
+
case "discord":
|
|
309
|
+
return { label: "channel or user ID", placeholder: "#approvals" };
|
|
310
|
+
case "msteams":
|
|
311
|
+
return { label: "channel or user", placeholder: "user@company.com" };
|
|
312
|
+
case "matrix":
|
|
313
|
+
return { label: "room or user", placeholder: "@admin:matrix.org" };
|
|
314
|
+
default:
|
|
315
|
+
return { label: "recipient identifier", placeholder: "admin" };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function printManualConfig(channel?: string, target?: string): void {
|
|
320
|
+
console.log(` "plugins": {`);
|
|
321
|
+
console.log(` "entries": {`);
|
|
322
|
+
console.log(` "${PLUGIN_ID}": {`);
|
|
323
|
+
console.log(` "enabled": true,`);
|
|
324
|
+
console.log(` "config": {`);
|
|
325
|
+
console.log(
|
|
326
|
+
` "adminTarget": "${target || "<your-admin-contact>"}"`,
|
|
327
|
+
);
|
|
328
|
+
console.log(
|
|
329
|
+
` "channel": "${channel || "<whatsapp|telegram|slack|...>"}"`,
|
|
330
|
+
);
|
|
331
|
+
console.log(` }`);
|
|
332
|
+
console.log(` }`);
|
|
333
|
+
console.log(` }`);
|
|
334
|
+
console.log(` }`);
|
|
335
|
+
}
|