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.
Files changed (2) hide show
  1. package/index.ts +258 -10
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,8 +1,43 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { join } from "node:path";
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 config = api.getConfig?.() ?? {};
28
- const adminTarget = config.adminTarget;
29
- const channel = config.channel;
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
- api.logger?.warn(
33
- `[exec-grant] Missing config. Set adminTarget and channel:\n` +
34
- ` openclaw config set plugins.entries.${PLUGIN_ID}.config.adminTarget "<target>"\n` +
35
- ` openclaw config set plugins.entries.${PLUGIN_ID}.config.channel "<channel>"\n` +
36
- ` Example channels: whatsapp, telegram, slack, discord, signal`,
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-plugin-exec-grant",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Time-boxed elevated shell access with WhatsApp admin approval for OpenClaw",
5
5
  "main": "index.ts",
6
6
  "license": "MIT",