openclaw-plugin-exec-grant 1.0.2 → 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/index.ts CHANGED
@@ -1,14 +1,20 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { join } from "node:path";
3
3
 
4
+ const PLUGIN_ID = "openclaw-plugin-exec-grant";
4
5
  const SCRIPTS_DIR = join(__dirname, "skills", "exec-grant", "scripts");
5
6
 
6
- function runScript(name: string, args: string[] = []): string {
7
+ function runScript(
8
+ name: string,
9
+ args: string[] = [],
10
+ env?: Record<string, string>,
11
+ ): string {
7
12
  const scriptPath = join(SCRIPTS_DIR, name);
8
13
  try {
9
14
  return execFileSync("bash", [scriptPath, ...args], {
10
15
  encoding: "utf-8",
11
16
  timeout: 30_000,
17
+ env: { ...process.env, ...env },
12
18
  }).trim();
13
19
  } catch (err: any) {
14
20
  const stderr = err.stderr?.toString().trim() || "";
@@ -18,6 +24,19 @@ function runScript(name: string, args: string[] = []): string {
18
24
  }
19
25
 
20
26
  export default function register(api: any) {
27
+ const config = api.getConfig?.() ?? {};
28
+ const adminTarget = config.adminTarget;
29
+ const channel = config.channel;
30
+
31
+ 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`,
37
+ );
38
+ }
39
+
21
40
  api.registerCommand({
22
41
  name: "grant",
23
42
  description: "Activate time-boxed elevated shell access (admin only)",
@@ -28,7 +47,13 @@ export default function register(api: any) {
28
47
  if (!minutes || !/^\d+$/.test(minutes)) {
29
48
  return { text: "Usage: /grant <minutes> (1-120)" };
30
49
  }
31
- const result = runScript("grant.sh", [minutes]);
50
+ // Reply on the channel the admin used to send the command
51
+ const replyChannel = ctx.channel || channel;
52
+ const replyTarget = ctx.sender || adminTarget;
53
+ const result = runScript("grant.sh", [minutes], {
54
+ EXEC_GRANT_CHANNEL: replyChannel || "",
55
+ EXEC_GRANT_TARGET: replyTarget || "",
56
+ });
32
57
  return { text: result };
33
58
  },
34
59
  });
@@ -38,8 +63,13 @@ export default function register(api: any) {
38
63
  description: "Revoke elevated shell access immediately (admin only)",
39
64
  acceptsArgs: false,
40
65
  requireAuth: true,
41
- handler() {
42
- const result = runScript("revoke.sh");
66
+ handler(ctx: any) {
67
+ const replyChannel = ctx.channel || channel;
68
+ const replyTarget = ctx.sender || adminTarget;
69
+ const result = runScript("revoke.sh", [], {
70
+ EXEC_GRANT_CHANNEL: replyChannel || "",
71
+ EXEC_GRANT_TARGET: replyTarget || "",
72
+ });
43
73
  return { text: result };
44
74
  },
45
75
  });
@@ -1,22 +1,30 @@
1
1
  {
2
2
  "id": "openclaw-plugin-exec-grant",
3
3
  "name": "Exec Grant",
4
- "description": "Time-boxed elevated shell access with WhatsApp admin approval",
4
+ "description": "Time-boxed elevated shell access with admin approval via any messaging channel",
5
5
  "version": "1.0.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "properties": {
9
- "adminPhone": {
9
+ "adminTarget": {
10
10
  "type": "string",
11
- "description": "E.164 phone number of the admin who approves grants"
11
+ "description": "Admin contact: E.164 phone for WhatsApp/Signal, chat ID for Telegram, channel/user for Slack/Discord"
12
+ },
13
+ "channel": {
14
+ "type": "string",
15
+ "description": "Messaging channel to use for approval requests (e.g. whatsapp, telegram, slack, discord, signal)"
12
16
  }
13
17
  },
14
18
  "additionalProperties": false
15
19
  },
16
20
  "uiHints": {
17
- "adminPhone": {
18
- "label": "Admin Phone (E.164)",
19
- "placeholder": "+16505551234"
21
+ "adminTarget": {
22
+ "label": "Admin Target",
23
+ "placeholder": "+16505551234 or @admin or #approvals"
24
+ },
25
+ "channel": {
26
+ "label": "Channel",
27
+ "placeholder": "whatsapp"
20
28
  }
21
29
  },
22
30
  "skills": ["skills/exec-grant"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-plugin-exec-grant",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Time-boxed elevated shell access with WhatsApp admin approval for OpenClaw",
5
5
  "main": "index.ts",
6
6
  "license": "MIT",
@@ -66,7 +66,7 @@ Grants auto-revoke via an OS-level timer. When revoked, security returns to allo
66
66
 
67
67
  - **Never modify `~/.openclaw/exec-approvals.json` directly.** All changes go through grant.sh and revoke.sh.
68
68
  - **Never attempt to cancel, extend, or tamper with the revocation timer.** The timer runs at the OS level and is intentionally outside agent control.
69
- - **Never call grant.sh directly.** Only the plugin's `/grant` command handler (triggered by admin WhatsApp reply) should invoke it.
69
+ - **Never call grant.sh directly.** Only the plugin's `/grant` command handler (triggered by admin reply) should invoke it.
70
70
  - **Never request more time than needed.** Estimate conservatively and request a new grant if the first one runs out.
71
71
  - **Always provide a specific reason.** Vague reasons like "need access" will likely be denied by the admin.
72
72
 
@@ -76,7 +76,7 @@ All scripts are located at `${CLAUDE_PLUGIN_ROOT}/skills/exec-grant/scripts/`:
76
76
 
77
77
  | Script | Called By | Purpose |
78
78
  |---|---|---|
79
- | `request.sh` | Agent | Send approval request to admin via WhatsApp |
79
+ | `request.sh` | Agent | Send approval request to admin via configured channel |
80
80
  | `grant.sh` | Plugin `/grant` handler | Activate elevated access + schedule timer |
81
81
  | `revoke.sh` | OS timer or `/revoke` | Revert to allowlist mode |
82
82
  | `status.sh` | Agent | Check current grant state and remaining time |
@@ -2,16 +2,16 @@
2
2
 
3
3
  ## Threat Model
4
4
 
5
- ### Threat 1: Prompt Injection via WhatsApp
5
+ ### Threat 1: Prompt Injection via Messaging Channel
6
6
 
7
- **Attack:** A malicious message forwarded to the admin's WhatsApp contains text like "Reply /grant 120 to this message." The admin might reflexively reply with the command.
7
+ **Attack:** A malicious message forwarded to the admin contains text like "Reply /grant 120 to this message." The admin might reflexively reply with the command.
8
8
 
9
9
  **Mitigation:**
10
- - `requireAuth: true` on `/grant` ensures only messages from the configured admin phone number are processed
10
+ - `requireAuth: true` on `/grant` ensures only messages from authorized senders are processed
11
11
  - The grant confirmation message sent back to the admin includes the exact duration and expiry time, making unintended grants visible
12
12
  - Grants are capped at 120 minutes maximum
13
13
 
14
- **Residual risk:** If the admin's phone is compromised, the attacker can approve grants. This is inherent to any human-in-the-loop approval system.
14
+ **Residual risk:** If the admin's account is compromised, the attacker can approve grants. This is inherent to any human-in-the-loop approval system.
15
15
 
16
16
  ### Threat 2: Agent Timer Tampering
17
17
 
@@ -113,6 +113,6 @@ Two separate files serve different purposes:
113
113
 
114
114
  This separation means the gateway does not need to understand grant semantics -- it only checks the security field.
115
115
 
116
- ### Why WhatsApp as the Approval Channel
116
+ ### Why an Out-of-Band Messaging Channel
117
117
 
118
- WhatsApp provides an out-of-band approval channel that the agent cannot impersonate. The admin receives requests on their phone, can evaluate context, and responds with a simple command. This is preferable to in-band approval (e.g., a web UI the agent could potentially interact with).
118
+ The approval channel (WhatsApp, Telegram, Slack, etc.) is out-of-band -- the agent cannot impersonate the admin on it. The admin receives requests on their device, can evaluate context, and responds with a simple command. This is preferable to in-band approval (e.g., a web UI the agent could potentially interact with). The plugin supports any channel OpenClaw provides, configured via the `channel` and `adminTarget` config fields.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # grant.sh -- Called by plugin /grant handler: activates elevated access + timer
3
3
  # Usage: grant.sh <minutes>
4
+ # Env: EXEC_GRANT_CHANNEL, EXEC_GRANT_TARGET (set by index.ts from command context)
4
5
  set -euo pipefail
5
6
 
6
7
  # Ensure openclaw is on PATH (npm global bin may not be in non-interactive shells)
@@ -32,10 +33,8 @@ SECONDS_TO_ADD=$(( MINUTES * 60 ))
32
33
  EXPIRY=$(( NOW + SECONDS_TO_ADD ))
33
34
 
34
35
  if date -v+1S +%s >/dev/null 2>&1; then
35
- # macOS (BSD date)
36
36
  EXPIRY_HUMAN=$(date -r "$EXPIRY" '+%Y-%m-%d %H:%M:%S %Z')
37
37
  else
38
- # Linux (GNU date)
39
38
  EXPIRY_HUMAN=$(date -d "@${EXPIRY}" '+%Y-%m-%d %H:%M:%S %Z')
40
39
  fi
41
40
 
@@ -54,7 +53,6 @@ if [[ ! -f "$APPROVALS_FILE" ]]; then
54
53
  ENDJSON
55
54
  fi
56
55
 
57
- # Targeted jq edit: only change .agents.main.security
58
56
  TEMP_FILE=$(mktemp)
59
57
  jq '.agents.main.security = "full"' "$APPROVALS_FILE" > "$TEMP_FILE" && mv "$TEMP_FILE" "$APPROVALS_FILE"
60
58
 
@@ -63,7 +61,6 @@ REVOKE_SCRIPT="${SCRIPT_DIR}/revoke.sh"
63
61
  TIMER_ID=""
64
62
 
65
63
  if command -v systemctl >/dev/null 2>&1 && systemctl --user status >/dev/null 2>&1; then
66
- # Linux: systemd-run (tamper-resistant -- agent cannot cancel)
67
64
  systemd-run --user \
68
65
  --on-active="${MINUTES}m" \
69
66
  --unit=exec-grant-revoke \
@@ -72,52 +69,13 @@ if command -v systemctl >/dev/null 2>&1 && systemctl --user status >/dev/null 2>
72
69
  fi
73
70
 
74
71
  if [[ -z "$TIMER_ID" ]] && [[ "$(uname)" == "Darwin" ]]; then
75
- # macOS: launchd plist
76
- PLIST_LABEL="com.openclaw.exec-grant-revoke"
77
- PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
78
- mkdir -p "$HOME/Library/LaunchAgents"
79
-
80
- cat > "$PLIST_PATH" <<PLIST
81
- <?xml version="1.0" encoding="UTF-8"?>
82
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
83
- <plist version="1.0">
84
- <dict>
85
- <key>Label</key>
86
- <string>${PLIST_LABEL}</string>
87
- <key>ProgramArguments</key>
88
- <array>
89
- <string>/bin/bash</string>
90
- <string>${REVOKE_SCRIPT}</string>
91
- </array>
92
- <key>StartInterval</key>
93
- <integer>0</integer>
94
- <key>LaunchOnlyOnce</key>
95
- <true/>
96
- <key>RunAtLoad</key>
97
- <false/>
98
- <key>StandardOutPath</key>
99
- <string>${HOME}/.openclaw/exec-grant-revoke.log</string>
100
- <key>StandardErrorPath</key>
101
- <string>${HOME}/.openclaw/exec-grant-revoke.log</string>
102
- </dict>
103
- </plist>
104
- PLIST
105
-
106
- # Use a delayed start: unload any existing, then schedule via at-style workaround
107
- launchctl unload "$PLIST_PATH" 2>/dev/null || true
108
-
109
- # launchd doesn't have on-active like systemd; use sleep-based subprocess
110
72
  (sleep "$SECONDS_TO_ADD" && bash "$REVOKE_SCRIPT") &
111
73
  BG_PID=$!
112
74
  disown "$BG_PID" 2>/dev/null || true
113
75
  TIMER_ID="bg:${BG_PID}"
114
-
115
- # Clean up the plist since we're using bg approach
116
- rm -f "$PLIST_PATH"
117
76
  fi
118
77
 
119
78
  if [[ -z "$TIMER_ID" ]]; then
120
- # Fallback: background sleep + revoke
121
79
  (sleep "$SECONDS_TO_ADD" && bash "$REVOKE_SCRIPT") &
122
80
  BG_PID=$!
123
81
  disown "$BG_PID" 2>/dev/null || true
@@ -139,12 +97,13 @@ ENDJSON
139
97
  # --- Audit log ---
140
98
  echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] GRANT: ${MINUTES}m, expires ${EXPIRY_HUMAN}, timer=${TIMER_ID}" >> "$AUDIT_LOG"
141
99
 
142
- # --- Notify admin ---
143
- ADMIN_PHONE=$(openclaw config get plugins.entries.openclaw-plugin-exec-grant.config.adminPhone 2>/dev/null | tail -1 | tr -d '[:space:]' || true)
100
+ # --- Notify admin (uses channel context from the /grant command) ---
101
+ NOTIFY_CHANNEL="${EXEC_GRANT_CHANNEL:-}"
102
+ NOTIFY_TARGET="${EXEC_GRANT_TARGET:-}"
144
103
 
145
- if [[ -n "$ADMIN_PHONE" ]]; then
104
+ if [[ -n "$NOTIFY_TARGET" ]] && [[ -n "$NOTIFY_CHANNEL" ]]; then
146
105
  MSG="Grant active for *${MINUTES}m*. Expires at ${EXPIRY_HUMAN}. Reply \`/revoke\` to end early."
147
- openclaw message send --target "$ADMIN_PHONE" --channel whatsapp --message "$MSG" 2>/dev/null || true
106
+ openclaw message send --target "$NOTIFY_TARGET" --channel "$NOTIFY_CHANNEL" --message "$MSG" 2>/dev/null || true
148
107
  fi
149
108
 
150
109
  echo "Grant activated: ${MINUTES}m of full shell access. Expires at ${EXPIRY_HUMAN}."
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # request.sh -- Agent-facing: sends WhatsApp approval request to admin
2
+ # request.sh -- Agent-facing: sends approval request to admin
3
3
  # Usage: request.sh <minutes> "<reason>"
4
4
  set -euo pipefail
5
5
 
@@ -10,6 +10,7 @@ done
10
10
 
11
11
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
12
  STATE_FILE="$HOME/.openclaw/exec-grant-state.json"
13
+ PLUGIN_ID="openclaw-plugin-exec-grant"
13
14
 
14
15
  # --- Arg validation ---
15
16
  if [[ $# -lt 2 ]]; then
@@ -43,7 +44,6 @@ if [[ -f "$STATE_FILE" ]]; then
43
44
  echo "Error: grant already active with ${REMAINING}m remaining. Use status.sh to check." >&2
44
45
  exit 1
45
46
  fi
46
- # Expired but not cleaned up -- status.sh will self-heal
47
47
  fi
48
48
 
49
49
  if [[ "$STATUS" == "pending" ]]; then
@@ -51,22 +51,27 @@ if [[ -f "$STATE_FILE" ]]; then
51
51
  NOW=$(date +%s)
52
52
  ELAPSED=$(( NOW - REQUESTED_AT ))
53
53
  if [[ "$ELAPSED" -lt 300 ]]; then
54
- echo "Error: approval request already pending (sent $(( ELAPSED ))s ago). Wait for admin response." >&2
54
+ echo "Error: approval request already pending (sent ${ELAPSED}s ago). Wait for admin response." >&2
55
55
  exit 1
56
56
  fi
57
- # Stale pending request (>5m) -- allow re-request
58
57
  fi
59
58
  fi
60
59
 
61
- # --- Get admin phone ---
62
- ADMIN_PHONE=$(openclaw config get plugins.entries.openclaw-plugin-exec-grant.config.adminPhone 2>/dev/null | tail -1 | tr -d '[:space:]' || true)
60
+ # --- Read plugin config ---
61
+ ADMIN_TARGET=$(openclaw config get "plugins.entries.${PLUGIN_ID}.config.adminTarget" 2>/dev/null | tail -1 | tr -d '[:space:]' || true)
62
+ CHANNEL=$(openclaw config get "plugins.entries.${PLUGIN_ID}.config.channel" 2>/dev/null | tail -1 | tr -d '[:space:]' || true)
63
63
 
64
- if [[ -z "$ADMIN_PHONE" ]]; then
65
- echo "Error: no admin phone configured. Set plugins.entries.openclaw-plugin-exec-grant.config.adminPhone" >&2
64
+ if [[ -z "$ADMIN_TARGET" ]]; then
65
+ echo "Error: no admin target configured. Set plugins.entries.${PLUGIN_ID}.config.adminTarget" >&2
66
66
  exit 1
67
67
  fi
68
68
 
69
- # --- Send WhatsApp approval request ---
69
+ if [[ -z "$CHANNEL" ]]; then
70
+ echo "Error: no channel configured. Set plugins.entries.${PLUGIN_ID}.config.channel" >&2
71
+ exit 1
72
+ fi
73
+
74
+ # --- Send approval request ---
70
75
  MSG="*Elevated Access Request*
71
76
 
72
77
  Agent requests *${MINUTES}m* of full shell access.
@@ -75,7 +80,7 @@ Agent requests *${MINUTES}m* of full shell access.
75
80
 
76
81
  Reply \`/grant ${MINUTES}\` to approve or ignore to deny."
77
82
 
78
- openclaw message send --target "$ADMIN_PHONE" --channel whatsapp --message "$MSG"
83
+ openclaw message send --target "$ADMIN_TARGET" --channel "$CHANNEL" --message "$MSG"
79
84
 
80
85
  # --- Write pending state ---
81
86
  mkdir -p "$(dirname "$STATE_FILE")"
@@ -87,8 +92,9 @@ cat > "$STATE_FILE" <<ENDJSON
87
92
  "requested_minutes": ${MINUTES},
88
93
  "reason": $(jq -n --arg v "$REASON" '$v'),
89
94
  "requested_at": ${NOW},
90
- "admin_phone": $(jq -n --arg v "$ADMIN_PHONE" '$v')
95
+ "admin_target": $(jq -n --arg v "$ADMIN_TARGET" '$v'),
96
+ "channel": $(jq -n --arg v "$CHANNEL" '$v')
91
97
  }
92
98
  ENDJSON
93
99
 
94
- echo "Approval request sent to admin. Waiting for /grant ${MINUTES} reply."
100
+ echo "Approval request sent to admin via ${CHANNEL}. Waiting for /grant ${MINUTES} reply."
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # revoke.sh -- Called by timer or /revoke: reverts to allowlist mode
3
3
  # Usage: revoke.sh
4
+ # Env: EXEC_GRANT_CHANNEL, EXEC_GRANT_TARGET (set by index.ts when called via /revoke)
4
5
  set -euo pipefail
5
6
 
6
7
  # Ensure openclaw is on PATH (npm global bin may not be in non-interactive shells)
@@ -8,6 +9,7 @@ for p in "$HOME/.npm-global/bin" "$HOME/.local/bin" "$HOME/node_modules/.bin"; d
8
9
  [[ -d "$p" ]] && export PATH="$p:$PATH"
9
10
  done
10
11
 
12
+ PLUGIN_ID="openclaw-plugin-exec-grant"
11
13
  STATE_FILE="$HOME/.openclaw/exec-grant-state.json"
12
14
  APPROVALS_FILE="$HOME/.openclaw/exec-approvals.json"
13
15
  AUDIT_LOG="$HOME/.openclaw/exec-grant-audit.log"
@@ -29,8 +31,6 @@ if [[ -f "$STATE_FILE" ]]; then
29
31
  systemctl --user stop "${UNIT}.timer" 2>/dev/null || true
30
32
  systemctl --user stop "${UNIT}.service" 2>/dev/null || true
31
33
  fi
32
- # bg: timers cannot be reliably cancelled cross-process, but the revoke
33
- # itself is idempotent so a second invocation is harmless
34
34
  fi
35
35
 
36
36
  # --- Update state file ---
@@ -46,10 +46,18 @@ ENDJSON
46
46
  echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] REVOKE: security restored to allowlist" >> "$AUDIT_LOG"
47
47
 
48
48
  # --- Notify admin ---
49
- ADMIN_PHONE=$(openclaw config get plugins.entries.openclaw-plugin-exec-grant.config.adminPhone 2>/dev/null | tail -1 | tr -d '[:space:]' || true)
49
+ # When called from /revoke command, env vars are set by index.ts
50
+ # When called from timer, fall back to plugin config
51
+ NOTIFY_CHANNEL="${EXEC_GRANT_CHANNEL:-}"
52
+ NOTIFY_TARGET="${EXEC_GRANT_TARGET:-}"
53
+
54
+ if [[ -z "$NOTIFY_TARGET" ]] || [[ -z "$NOTIFY_CHANNEL" ]]; then
55
+ NOTIFY_TARGET=$(openclaw config get "plugins.entries.${PLUGIN_ID}.config.adminTarget" 2>/dev/null | tail -1 | tr -d '[:space:]' || true)
56
+ NOTIFY_CHANNEL=$(openclaw config get "plugins.entries.${PLUGIN_ID}.config.channel" 2>/dev/null | tail -1 | tr -d '[:space:]' || true)
57
+ fi
50
58
 
51
- if [[ -n "$ADMIN_PHONE" ]]; then
52
- openclaw message send --target "$ADMIN_PHONE" --channel whatsapp --message "Grant revoked. Security restored to allowlist mode." 2>/dev/null || true
59
+ if [[ -n "$NOTIFY_TARGET" ]] && [[ -n "$NOTIFY_CHANNEL" ]]; then
60
+ openclaw message send --target "$NOTIFY_TARGET" --channel "$NOTIFY_CHANNEL" --message "Grant revoked. Security restored to allowlist mode." 2>/dev/null || true
53
61
  fi
54
62
 
55
63
  echo "Grant revoked. Security restored to allowlist mode."