@vaultclaw/vaultclaw-mcp-approval-handoff 0.1.2

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/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # Vaultclaw MCP Approval Handoff Plugin
2
+
3
+ OpenClaw plugin that auto-handles Vaultclaw MCP approval handoff during agent tool runs.
4
+
5
+ When a tool returns `MCP_APPROVAL_REQUIRED`, the plugin:
6
+
7
+ - extracts `error.details.approval.next_action.arguments.handle`
8
+ - notifies the user to approve/deny in Vaultclaw UI
9
+ - asynchronously invokes `vaultclaw_approval_wait`
10
+ - posts a single terminal update for `ALLOW`, `DENY`, or timeout
11
+ - on `ALLOW`, auto-triggers one follow-up `agent` run for the same `sessionKey` so
12
+ compound flows continue without a manual "approved" message
13
+
14
+ No second manual CLI command is required.
15
+
16
+ ## Install
17
+
18
+ ### Option A: npm package
19
+
20
+ ```bash
21
+ openclaw plugins install @vaultclaw/vaultclaw-mcp-approval-handoff
22
+ ```
23
+
24
+ ### Option B: one-shot installer script (local checkout)
25
+
26
+ ```bash
27
+ ./scripts/install.sh
28
+ ```
29
+
30
+ Optional npm/package override:
31
+
32
+ ```bash
33
+ ./scripts/install.sh @vaultclaw/vaultclaw-mcp-approval-handoff
34
+ ```
35
+
36
+ ## Config
37
+
38
+ Plugin id: `vaultclaw-mcp-approval-handoff`
39
+
40
+ ```json
41
+ {
42
+ "plugins": {
43
+ "entries": {
44
+ "vaultclaw-mcp-approval-handoff": {
45
+ "enabled": true,
46
+ "config": {
47
+ "enabled": true,
48
+ "pollIntervalMs": 1500,
49
+ "maxWaitMs": 600000,
50
+ "commandTimeoutMs": 720000,
51
+ "maxConcurrentWaits": 10
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ Validation rules:
60
+
61
+ - `pollIntervalMs`: `250..10000`
62
+ - `maxWaitMs`: `1000..3600000`
63
+ - `commandTimeoutMs > maxWaitMs`
64
+ - `maxConcurrentWaits >= 1`
65
+
66
+ ## Behavior
67
+
68
+ - Scope: MCP tool results in OpenClaw agent runs.
69
+ - Non-blocking: wait worker runs async from `after_tool_call`.
70
+ - Supported handles: `JOB` and `PLAN_RUN`.
71
+ - Dedupe key: `(session_id, challenge_id, pending_id, run_id/job_id)`.
72
+ - Session lifecycle: pending waits are canceled on `before_reset` and `session_end`.
73
+ - Retries: transient transport failures retry with backoff (`1s, 2s, 4s, 8s, 16s`, max 5 attempts).
74
+ - No-retry terminal categories: validation/auth/timeouts.
75
+
76
+ ## Operational Limits
77
+
78
+ - Wait calls are executed through the local Gateway HTTP route: `POST /tools/invoke`.
79
+ - Gateway auth must allow that route (`token`/`password` via config or env, or `none`/`trusted-proxy` mode).
80
+ - Status updates are posted through system events + heartbeat wake and require a valid `sessionKey`.
81
+ - At most `maxConcurrentWaits` approval workers run in parallel.
82
+
83
+ ## Observability
84
+
85
+ Structured plugin logs include:
86
+
87
+ - `approval_detected`
88
+ - `wait_started`
89
+ - `wait_completed`
90
+ - `wait_failed`
91
+ - `wait_canceled`
92
+ - `wait_retry`
93
+ - `terminal_outcome`
94
+ - `resume_started`
95
+ - `resume_completed`
96
+ - `resume_failed`
97
+ - `cleanup`
98
+
99
+ Each log includes correlation keys when available:
100
+
101
+ - `session_id`
102
+ - `challenge_id`
103
+ - `pending_id`
104
+ - `run_id` / `job_id`
105
+
106
+ ## Example Transcript
107
+
108
+ 1. User: "Trash the newsletter in Gmail from yesterday."
109
+ 2. Tool result: approval challenge returned (`MCP_APPROVAL_REQUIRED`).
110
+ 3. Plugin post: "Approval required in Vaultclaw UI. Waiting up to 10 minutes... (challenge_id=..., pending_id=..., run_id=...)"
111
+ 4. User approves in Vaultclaw UI.
112
+ 5. Plugin post: "Approval allowed in Vaultclaw UI. Continuing automatically. (...ids...)"
113
+
114
+ ## Development
115
+
116
+ ```bash
117
+ npm install
118
+ npm test
119
+ ```
package/index.ts ADDED
@@ -0,0 +1,127 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { ApprovalHandoffManager } from "./src/approval-manager.js";
3
+ import { normalizePluginConfig } from "./src/config.js";
4
+ import { createGatewayAgentResumeInvoker } from "./src/resume-invoker.js";
5
+ import { createGatewayToolsInvokeWaitInvoker } from "./src/wait-invoker.js";
6
+
7
+ const plugin = {
8
+ id: "vaultclaw-mcp-approval-handoff",
9
+ name: "Vaultclaw MCP Approval Handoff",
10
+ description: "Automatically waits MCP approval challenges and posts terminal outcomes.",
11
+ configSchema: {
12
+ parse(value: unknown) {
13
+ return normalizePluginConfig(value);
14
+ },
15
+ uiHints: {
16
+ enabled: {
17
+ label: "Enable Auto Approval Wait",
18
+ },
19
+ pollIntervalMs: {
20
+ label: "Poll Interval (ms)",
21
+ },
22
+ maxWaitMs: {
23
+ label: "Max Wait (ms)",
24
+ },
25
+ commandTimeoutMs: {
26
+ label: "Command Timeout (ms)",
27
+ },
28
+ maxConcurrentWaits: {
29
+ label: "Max Concurrent Waits",
30
+ },
31
+ },
32
+ },
33
+ register(api: OpenClawPluginApi) {
34
+ const resolveSessionKey = (ctx: any): string | undefined => {
35
+ const explicit = typeof ctx?.sessionKey === "string" ? ctx.sessionKey.trim() : "";
36
+ if (explicit) {
37
+ return explicit;
38
+ }
39
+ const sessionId = typeof ctx?.sessionId === "string" ? ctx.sessionId.trim() : "";
40
+ if (sessionId && sessionId.startsWith("agent:")) {
41
+ return sessionId;
42
+ }
43
+ const agentId = typeof ctx?.agentId === "string" ? ctx.agentId.trim() : "";
44
+ if (agentId) {
45
+ return `agent:${agentId}:main`;
46
+ }
47
+ return "agent:main:main";
48
+ };
49
+
50
+ const config = normalizePluginConfig(api.pluginConfig);
51
+ const waitInvoker = createGatewayToolsInvokeWaitInvoker(api.config);
52
+ const resumeInvoker = typeof api.runtime?.system?.runCommandWithTimeout === "function"
53
+ ? createGatewayAgentResumeInvoker({
54
+ runCommandWithTimeout: api.runtime.system.runCommandWithTimeout,
55
+ })
56
+ : undefined;
57
+
58
+ const manager = new ApprovalHandoffManager({
59
+ config,
60
+ waitInvoker,
61
+ resumeInvoker,
62
+ logger: api.logger,
63
+ notifier: {
64
+ post: ({ sessionKey, text, reason, contextKey }) => {
65
+ const scopedSessionKey = sessionKey?.trim();
66
+ if (!scopedSessionKey) {
67
+ api.logger.warn(
68
+ `[vaultclaw-approval-handoff] skipped notification with no sessionKey: ${text}`,
69
+ );
70
+ return;
71
+ }
72
+ try {
73
+ api.runtime.system.enqueueSystemEvent(text, {
74
+ sessionKey: scopedSessionKey,
75
+ contextKey,
76
+ });
77
+ const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
78
+ if (typeof requestHeartbeatNow === "function") {
79
+ requestHeartbeatNow({
80
+ reason: `vaultclaw-approval-handoff:${reason}`,
81
+ sessionKey: scopedSessionKey,
82
+ });
83
+ }
84
+ } catch (error) {
85
+ api.logger.warn(
86
+ `[vaultclaw-approval-handoff] failed to enqueue system event: ${String(error)}`,
87
+ );
88
+ }
89
+ },
90
+ },
91
+ });
92
+
93
+ api.on("after_tool_call", (event: any, ctx: any) => {
94
+ const sessionKey = resolveSessionKey(ctx);
95
+ manager.onAfterToolCall(
96
+ {
97
+ toolName: event.toolName,
98
+ result: event.result,
99
+ runId: event.runId,
100
+ },
101
+ {
102
+ sessionId: ctx.sessionId ?? sessionKey,
103
+ sessionKey,
104
+ runId: ctx.runId,
105
+ },
106
+ );
107
+ });
108
+
109
+ api.on("before_reset", (event: any, ctx: any) => {
110
+ const sessionKey = resolveSessionKey(ctx);
111
+ manager.onBeforeReset(event, {
112
+ sessionId: ctx.sessionId ?? sessionKey,
113
+ sessionKey,
114
+ });
115
+ });
116
+
117
+ api.on("session_end", (event: any, ctx: any) => {
118
+ const sessionKey = resolveSessionKey(ctx);
119
+ manager.onSessionEnd(event, {
120
+ sessionId: ctx.sessionId ?? sessionKey,
121
+ sessionKey,
122
+ });
123
+ });
124
+ },
125
+ };
126
+
127
+ export default plugin;
@@ -0,0 +1,57 @@
1
+ {
2
+ "id": "vaultclaw-mcp-approval-handoff",
3
+ "name": "Vaultclaw MCP Approval Handoff",
4
+ "description": "Auto-resume Vaultclaw MCP approval-required tool flows.",
5
+ "version": "0.1.0",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "enabled": {
11
+ "type": "boolean",
12
+ "default": true
13
+ },
14
+ "pollIntervalMs": {
15
+ "type": "integer",
16
+ "default": 1500,
17
+ "minimum": 250,
18
+ "maximum": 10000
19
+ },
20
+ "maxWaitMs": {
21
+ "type": "integer",
22
+ "default": 600000,
23
+ "minimum": 1000,
24
+ "maximum": 3600000
25
+ },
26
+ "commandTimeoutMs": {
27
+ "type": "integer",
28
+ "default": 720000,
29
+ "minimum": 1001,
30
+ "maximum": 7200000
31
+ },
32
+ "maxConcurrentWaits": {
33
+ "type": "integer",
34
+ "default": 10,
35
+ "minimum": 1,
36
+ "maximum": 100
37
+ }
38
+ }
39
+ },
40
+ "uiHints": {
41
+ "enabled": {
42
+ "label": "Enable Auto Approval Wait"
43
+ },
44
+ "pollIntervalMs": {
45
+ "label": "Poll Interval (ms)"
46
+ },
47
+ "maxWaitMs": {
48
+ "label": "Max Wait (ms)"
49
+ },
50
+ "commandTimeoutMs": {
51
+ "label": "Command Timeout (ms)"
52
+ },
53
+ "maxConcurrentWaits": {
54
+ "label": "Max Concurrent Waits"
55
+ }
56
+ }
57
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@vaultclaw/vaultclaw-mcp-approval-handoff",
3
+ "version": "0.1.2",
4
+ "description": "OpenClaw plugin that auto-waits Vaultclaw MCP approvals",
5
+ "type": "module",
6
+ "files": [
7
+ "index.ts",
8
+ "openclaw.plugin.json",
9
+ "src",
10
+ "scripts/install.sh",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "test": "vitest run",
15
+ "test:watch": "vitest"
16
+ },
17
+ "dependencies": {
18
+ "openclaw": "^2026.3.0"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.9.2",
22
+ "vitest": "^3.2.4"
23
+ },
24
+ "openclaw": {
25
+ "extensions": [
26
+ "./index.ts"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ PLUGIN_ID="vaultclaw-mcp-approval-handoff"
5
+ SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
6
+ ROOT_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
7
+ EXT_DIR="${HOME}/.openclaw/extensions/${PLUGIN_ID}"
8
+
9
+ SPEC="${1:-$ROOT_DIR}"
10
+
11
+ echo "Installing plugin from: $SPEC"
12
+ if openclaw plugins info "$PLUGIN_ID" >/dev/null 2>&1; then
13
+ echo "Existing plugin detected, uninstalling old copy..."
14
+ openclaw plugins uninstall "$PLUGIN_ID" --force || true
15
+ fi
16
+ if [ -d "$EXT_DIR" ]; then
17
+ echo "Removing stale extension directory: $EXT_DIR"
18
+ rm -rf "$EXT_DIR"
19
+ fi
20
+
21
+ openclaw plugins install "$SPEC"
22
+ openclaw plugins enable "$PLUGIN_ID" || true
23
+
24
+ openclaw config set "plugins.entries.$PLUGIN_ID.config.enabled" true
25
+ openclaw config set "plugins.entries.$PLUGIN_ID.config.pollIntervalMs" 1500
26
+ openclaw config set "plugins.entries.$PLUGIN_ID.config.maxWaitMs" 600000
27
+ openclaw config set "plugins.entries.$PLUGIN_ID.config.commandTimeoutMs" 720000
28
+ openclaw config set "plugins.entries.$PLUGIN_ID.config.maxConcurrentWaits" 10
29
+
30
+ echo "Plugin installed and configured: $PLUGIN_ID"
31
+ echo "Restart the gateway to apply config if it is already running: openclaw gateway restart"