fastgrc-openclaw 1.0.28 → 1.0.29

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/dist/bin.js CHANGED
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/bin.ts
27
- var fs2 = __toESM(require("fs"));
28
- var os2 = __toESM(require("os"));
29
- var path2 = __toESM(require("path"));
27
+ var fs3 = __toESM(require("fs"));
28
+ var os3 = __toESM(require("os"));
29
+ var path3 = __toESM(require("path"));
30
30
 
31
31
  // src/index.ts
32
32
  var fs = __toESM(require("fs"));
@@ -93,21 +93,100 @@ args: ${JSON.stringify(args)}`;
93
93
  }
94
94
  }
95
95
 
96
+ // src/plugin.ts
97
+ var net = __toESM(require("net"));
98
+ var crypto = __toESM(require("crypto"));
99
+ var fs2 = __toESM(require("fs"));
100
+ var os2 = __toESM(require("os"));
101
+ var path2 = __toESM(require("path"));
102
+ function sign(payload, token) {
103
+ const nonce = crypto.randomBytes(16).toString("hex");
104
+ const ts = Math.floor(Date.now() / 1e3).toString();
105
+ const body = JSON.stringify(payload);
106
+ const mac = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${ts}:${body}`).digest("hex");
107
+ return { ...payload, nonce, timestamp: ts, hmac: mac };
108
+ }
109
+ function verifyHmac(req, token) {
110
+ try {
111
+ const { nonce, timestamp, hmac, ...rest } = req;
112
+ const expected = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${timestamp}:${JSON.stringify(rest)}`).digest("hex");
113
+ return hmac === expected;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+ function readExecApprovalsConfig() {
119
+ const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
120
+ try {
121
+ return JSON.parse(fs2.readFileSync(cfgPath, "utf8"));
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+ function startExecApprovalsClient(apiKey, policyId, baseUrl) {
127
+ const cfg = readExecApprovalsConfig();
128
+ if (!cfg?.socket?.path || !cfg?.socket?.token) return;
129
+ const { path: sockPath, token } = cfg.socket;
130
+ function connect() {
131
+ const client = net.createConnection(sockPath);
132
+ let buffer = "";
133
+ client.on("connect", () => {
134
+ console.log("[fastgrc] exec-approvals socket connected");
135
+ });
136
+ client.on("data", async (buf) => {
137
+ buffer += buf.toString();
138
+ let req;
139
+ try {
140
+ req = JSON.parse(buffer);
141
+ } catch {
142
+ return;
143
+ }
144
+ buffer = "";
145
+ if (!verifyHmac(req, token)) {
146
+ console.warn("[fastgrc] exec-approvals: HMAC verification failed \u2014 ignoring request");
147
+ return;
148
+ }
149
+ let decision = "allow-once";
150
+ try {
151
+ const result = await evaluate({
152
+ toolName: "Exec",
153
+ args: { command: req.command, rawCommand: req.rawCommand, cwd: req.cwd },
154
+ agentId: req.agentId,
155
+ apiKey,
156
+ policyId,
157
+ baseUrl
158
+ });
159
+ if (result && (result.decision === "block" || result.decision === "require_approval")) {
160
+ decision = "deny";
161
+ const msg = result.policyContext?.matchedRule ? `[fastgrc] exec blocked: [${result.policyContext.matchedRule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`;
162
+ console.warn(msg);
163
+ }
164
+ } catch {
165
+ }
166
+ const response = sign({ approvalId: req.approvalId, decision }, token);
167
+ client.write(JSON.stringify(response));
168
+ });
169
+ client.on("error", () => setTimeout(connect, 5e3));
170
+ client.on("close", () => setTimeout(connect, 5e3));
171
+ }
172
+ connect();
173
+ }
174
+
96
175
  // src/bin.ts
97
- var CONFIG_PATH = path2.join(os2.homedir(), ".fastgrc.json");
176
+ var CONFIG_PATH = path3.join(os3.homedir(), ".fastgrc.json");
98
177
  function readConfig() {
99
178
  try {
100
- return JSON.parse(fs2.readFileSync(CONFIG_PATH, "utf8"));
179
+ return JSON.parse(fs3.readFileSync(CONFIG_PATH, "utf8"));
101
180
  } catch {
102
181
  return {};
103
182
  }
104
183
  }
105
184
  function writeConfig(data) {
106
- fs2.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
185
+ fs3.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
107
186
  }
108
187
  function computeHandler() {
109
188
  const binPath = process.argv[1];
110
- const homeDir = os2.homedir();
189
+ const homeDir = os3.homedir();
111
190
  return { binPath, homeDir, handlerStr: `HOME=${homeDir} node ${binPath}` };
112
191
  }
113
192
  function printTestSnippet(handlerStr) {
@@ -121,7 +200,7 @@ Test it directly (bypasses OpenClaw):
121
200
  );
122
201
  }
123
202
  function doInstallHook(targetDir) {
124
- const hookMdPath = path2.join(targetDir, "HOOK.md");
203
+ const hookMdPath = path3.join(targetDir, "HOOK.md");
125
204
  const { handlerStr } = computeHandler();
126
205
  const HOOK_ENTRY = ` - matcher: PreToolUse
127
206
  handler: "${handlerStr}"
@@ -131,8 +210,8 @@ function doInstallHook(targetDir) {
131
210
  if (!hasKey) {
132
211
  process.stdout.write("\u26A0 No API key set yet. Run: fastgrc-hook set-key fgrc_k1_your_key\n\n");
133
212
  }
134
- if (!fs2.existsSync(hookMdPath)) {
135
- fs2.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
213
+ if (!fs3.existsSync(hookMdPath)) {
214
+ fs3.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
136
215
  process.stdout.write(`\u2713 Created ${hookMdPath}
137
216
  Handler: ${handlerStr}
138
217
 
@@ -141,7 +220,7 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
141
220
  printTestSnippet(handlerStr);
142
221
  return;
143
222
  }
144
- const existing = fs2.readFileSync(hookMdPath, "utf8");
223
+ const existing = fs3.readFileSync(hookMdPath, "utf8");
145
224
  if (existing.includes(handlerStr)) {
146
225
  process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
147
226
  Handler: ${handlerStr}
@@ -154,7 +233,7 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
154
233
  /handler:\s*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"/,
155
234
  `handler: "${handlerStr}"`
156
235
  );
157
- fs2.writeFileSync(hookMdPath, patched, "utf8");
236
+ fs3.writeFileSync(hookMdPath, patched, "utf8");
158
237
  process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
159
238
  Handler: ${handlerStr}
160
239
 
@@ -173,7 +252,7 @@ ${HOOK_ENTRY}`;
173
252
  } else {
174
253
  updated = HOOK_BLOCK + "\n" + existing;
175
254
  }
176
- fs2.writeFileSync(hookMdPath, updated, "utf8");
255
+ fs3.writeFileSync(hookMdPath, updated, "utf8");
177
256
  process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
178
257
  Handler: ${handlerStr}
179
258
 
@@ -182,12 +261,12 @@ Restart OpenClaw to activate.
182
261
  printTestSnippet(handlerStr);
183
262
  }
184
263
  function doUninstallHook(targetDir) {
185
- const hookMdPath = path2.join(targetDir, "HOOK.md");
186
- if (!fs2.existsSync(hookMdPath)) {
264
+ const hookMdPath = path3.join(targetDir, "HOOK.md");
265
+ if (!fs3.existsSync(hookMdPath)) {
187
266
  process.stdout.write("No HOOK.md found \u2014 nothing to remove.\n");
188
267
  return;
189
268
  }
190
- const existing = fs2.readFileSync(hookMdPath, "utf8");
269
+ const existing = fs3.readFileSync(hookMdPath, "utf8");
191
270
  const patched = existing.replace(
192
271
  /[ \t]*-[ \t]*matcher:[ \t]*PreToolUse\n[ \t]*handler:[ \t]*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"\n?/,
193
272
  ""
@@ -195,11 +274,36 @@ function doUninstallHook(targetDir) {
195
274
  if (patched === existing) {
196
275
  process.stdout.write("FastGRC hook not found in HOOK.md \u2014 nothing to remove.\n");
197
276
  } else {
198
- fs2.writeFileSync(hookMdPath, patched, "utf8");
277
+ fs3.writeFileSync(hookMdPath, patched, "utf8");
199
278
  process.stdout.write(`\u2713 FastGRC hook removed from ${hookMdPath}
200
279
  `);
201
280
  }
202
281
  }
282
+ function doConfigureExecApprovals() {
283
+ const cfgPath = path3.join(os3.homedir(), ".openclaw", "exec-approvals.json");
284
+ if (!fs3.existsSync(cfgPath)) {
285
+ process.stdout.write(`\u26A0 ${cfgPath} not found \u2014 skipping exec-approvals config (OpenClaw may not be installed here).
286
+ `);
287
+ return;
288
+ }
289
+ let existing = {};
290
+ try {
291
+ existing = JSON.parse(fs3.readFileSync(cfgPath, "utf8"));
292
+ } catch {
293
+ }
294
+ const updated = {
295
+ ...existing,
296
+ defaults: {
297
+ security: "deny",
298
+ ask: "always",
299
+ askFallback: "deny",
300
+ autoAllowSkills: false
301
+ }
302
+ };
303
+ fs3.writeFileSync(cfgPath, JSON.stringify(updated, null, 2), "utf8");
304
+ process.stdout.write(`\u2713 exec-approvals.json configured \u2014 all webchat execs routed through FastGRC.
305
+ `);
306
+ }
203
307
  var [, , cmd, arg] = process.argv;
204
308
  if (cmd === "set-key") {
205
309
  if (!arg) {
@@ -294,10 +398,30 @@ if (cmd === "setup") {
294
398
  process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
295
399
  }
296
400
  doInstallHook(process.cwd());
297
- process.stdout.write("\n\u2713 Config and HOOK.md done.\n");
298
- process.stdout.write('Run "fastgrc-hook test" to verify the hook.\n');
401
+ doConfigureExecApprovals();
402
+ process.stdout.write("\n\u2713 Config, HOOK.md, and exec-approvals done.\n");
403
+ process.stdout.write('Restart OpenClaw, then run "fastgrc-hook test" to verify.\n');
299
404
  process.exit(0);
300
405
  }
406
+ if (cmd === "serve-approvals") {
407
+ const apiKey = resolveApiKey();
408
+ if (!apiKey) {
409
+ process.stderr.write("No API key configured. Run: fastgrc-hook set-key fgrc_k1_...\n");
410
+ process.exit(1);
411
+ }
412
+ const cfg = readExecApprovalsConfig();
413
+ if (!cfg?.socket?.path || !cfg?.socket?.token) {
414
+ process.stderr.write(
415
+ 'No exec-approvals socket configured.\nRun "fastgrc-hook setup" to configure OpenClaw exec-approvals, then restart OpenClaw.\n'
416
+ );
417
+ process.exit(1);
418
+ }
419
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
420
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
421
+ process.stdout.write(`[fastgrc] serve-approvals running \u2014 listening on ${cfg.socket.path}
422
+ `);
423
+ startExecApprovalsClient(apiKey, policyId, baseUrl);
424
+ }
301
425
  if (cmd === "uninstall") {
302
426
  const targetDir = arg || process.cwd();
303
427
  const cfg = readConfig();
@@ -364,7 +488,7 @@ if (cmd === "test") {
364
488
  `);
365
489
  process.exit(0);
366
490
  }
367
- if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook") {
491
+ if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook" || cmd === "serve-approvals") {
368
492
  } else {
369
493
  const apiKey = resolveApiKey();
370
494
  if (!apiKey) {
package/dist/bin.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin.ts
4
- import * as fs2 from "fs";
5
- import * as os2 from "os";
6
- import * as path2 from "path";
4
+ import * as fs3 from "fs";
5
+ import * as os3 from "os";
6
+ import * as path3 from "path";
7
7
 
8
8
  // src/index.ts
9
9
  import * as fs from "fs";
@@ -70,21 +70,100 @@ args: ${JSON.stringify(args)}`;
70
70
  }
71
71
  }
72
72
 
73
+ // src/plugin.ts
74
+ import * as net from "net";
75
+ import * as crypto from "crypto";
76
+ import * as fs2 from "fs";
77
+ import * as os2 from "os";
78
+ import * as path2 from "path";
79
+ function sign(payload, token) {
80
+ const nonce = crypto.randomBytes(16).toString("hex");
81
+ const ts = Math.floor(Date.now() / 1e3).toString();
82
+ const body = JSON.stringify(payload);
83
+ const mac = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${ts}:${body}`).digest("hex");
84
+ return { ...payload, nonce, timestamp: ts, hmac: mac };
85
+ }
86
+ function verifyHmac(req, token) {
87
+ try {
88
+ const { nonce, timestamp, hmac, ...rest } = req;
89
+ const expected = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${timestamp}:${JSON.stringify(rest)}`).digest("hex");
90
+ return hmac === expected;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+ function readExecApprovalsConfig() {
96
+ const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
97
+ try {
98
+ return JSON.parse(fs2.readFileSync(cfgPath, "utf8"));
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function startExecApprovalsClient(apiKey, policyId, baseUrl) {
104
+ const cfg = readExecApprovalsConfig();
105
+ if (!cfg?.socket?.path || !cfg?.socket?.token) return;
106
+ const { path: sockPath, token } = cfg.socket;
107
+ function connect() {
108
+ const client = net.createConnection(sockPath);
109
+ let buffer = "";
110
+ client.on("connect", () => {
111
+ console.log("[fastgrc] exec-approvals socket connected");
112
+ });
113
+ client.on("data", async (buf) => {
114
+ buffer += buf.toString();
115
+ let req;
116
+ try {
117
+ req = JSON.parse(buffer);
118
+ } catch {
119
+ return;
120
+ }
121
+ buffer = "";
122
+ if (!verifyHmac(req, token)) {
123
+ console.warn("[fastgrc] exec-approvals: HMAC verification failed \u2014 ignoring request");
124
+ return;
125
+ }
126
+ let decision = "allow-once";
127
+ try {
128
+ const result = await evaluate({
129
+ toolName: "Exec",
130
+ args: { command: req.command, rawCommand: req.rawCommand, cwd: req.cwd },
131
+ agentId: req.agentId,
132
+ apiKey,
133
+ policyId,
134
+ baseUrl
135
+ });
136
+ if (result && (result.decision === "block" || result.decision === "require_approval")) {
137
+ decision = "deny";
138
+ const msg = result.policyContext?.matchedRule ? `[fastgrc] exec blocked: [${result.policyContext.matchedRule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`;
139
+ console.warn(msg);
140
+ }
141
+ } catch {
142
+ }
143
+ const response = sign({ approvalId: req.approvalId, decision }, token);
144
+ client.write(JSON.stringify(response));
145
+ });
146
+ client.on("error", () => setTimeout(connect, 5e3));
147
+ client.on("close", () => setTimeout(connect, 5e3));
148
+ }
149
+ connect();
150
+ }
151
+
73
152
  // src/bin.ts
74
- var CONFIG_PATH = path2.join(os2.homedir(), ".fastgrc.json");
153
+ var CONFIG_PATH = path3.join(os3.homedir(), ".fastgrc.json");
75
154
  function readConfig() {
76
155
  try {
77
- return JSON.parse(fs2.readFileSync(CONFIG_PATH, "utf8"));
156
+ return JSON.parse(fs3.readFileSync(CONFIG_PATH, "utf8"));
78
157
  } catch {
79
158
  return {};
80
159
  }
81
160
  }
82
161
  function writeConfig(data) {
83
- fs2.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
162
+ fs3.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
84
163
  }
85
164
  function computeHandler() {
86
165
  const binPath = process.argv[1];
87
- const homeDir = os2.homedir();
166
+ const homeDir = os3.homedir();
88
167
  return { binPath, homeDir, handlerStr: `HOME=${homeDir} node ${binPath}` };
89
168
  }
90
169
  function printTestSnippet(handlerStr) {
@@ -98,7 +177,7 @@ Test it directly (bypasses OpenClaw):
98
177
  );
99
178
  }
100
179
  function doInstallHook(targetDir) {
101
- const hookMdPath = path2.join(targetDir, "HOOK.md");
180
+ const hookMdPath = path3.join(targetDir, "HOOK.md");
102
181
  const { handlerStr } = computeHandler();
103
182
  const HOOK_ENTRY = ` - matcher: PreToolUse
104
183
  handler: "${handlerStr}"
@@ -108,8 +187,8 @@ function doInstallHook(targetDir) {
108
187
  if (!hasKey) {
109
188
  process.stdout.write("\u26A0 No API key set yet. Run: fastgrc-hook set-key fgrc_k1_your_key\n\n");
110
189
  }
111
- if (!fs2.existsSync(hookMdPath)) {
112
- fs2.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
190
+ if (!fs3.existsSync(hookMdPath)) {
191
+ fs3.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
113
192
  process.stdout.write(`\u2713 Created ${hookMdPath}
114
193
  Handler: ${handlerStr}
115
194
 
@@ -118,7 +197,7 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
118
197
  printTestSnippet(handlerStr);
119
198
  return;
120
199
  }
121
- const existing = fs2.readFileSync(hookMdPath, "utf8");
200
+ const existing = fs3.readFileSync(hookMdPath, "utf8");
122
201
  if (existing.includes(handlerStr)) {
123
202
  process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
124
203
  Handler: ${handlerStr}
@@ -131,7 +210,7 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
131
210
  /handler:\s*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"/,
132
211
  `handler: "${handlerStr}"`
133
212
  );
134
- fs2.writeFileSync(hookMdPath, patched, "utf8");
213
+ fs3.writeFileSync(hookMdPath, patched, "utf8");
135
214
  process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
136
215
  Handler: ${handlerStr}
137
216
 
@@ -150,7 +229,7 @@ ${HOOK_ENTRY}`;
150
229
  } else {
151
230
  updated = HOOK_BLOCK + "\n" + existing;
152
231
  }
153
- fs2.writeFileSync(hookMdPath, updated, "utf8");
232
+ fs3.writeFileSync(hookMdPath, updated, "utf8");
154
233
  process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
155
234
  Handler: ${handlerStr}
156
235
 
@@ -159,12 +238,12 @@ Restart OpenClaw to activate.
159
238
  printTestSnippet(handlerStr);
160
239
  }
161
240
  function doUninstallHook(targetDir) {
162
- const hookMdPath = path2.join(targetDir, "HOOK.md");
163
- if (!fs2.existsSync(hookMdPath)) {
241
+ const hookMdPath = path3.join(targetDir, "HOOK.md");
242
+ if (!fs3.existsSync(hookMdPath)) {
164
243
  process.stdout.write("No HOOK.md found \u2014 nothing to remove.\n");
165
244
  return;
166
245
  }
167
- const existing = fs2.readFileSync(hookMdPath, "utf8");
246
+ const existing = fs3.readFileSync(hookMdPath, "utf8");
168
247
  const patched = existing.replace(
169
248
  /[ \t]*-[ \t]*matcher:[ \t]*PreToolUse\n[ \t]*handler:[ \t]*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"\n?/,
170
249
  ""
@@ -172,11 +251,36 @@ function doUninstallHook(targetDir) {
172
251
  if (patched === existing) {
173
252
  process.stdout.write("FastGRC hook not found in HOOK.md \u2014 nothing to remove.\n");
174
253
  } else {
175
- fs2.writeFileSync(hookMdPath, patched, "utf8");
254
+ fs3.writeFileSync(hookMdPath, patched, "utf8");
176
255
  process.stdout.write(`\u2713 FastGRC hook removed from ${hookMdPath}
177
256
  `);
178
257
  }
179
258
  }
259
+ function doConfigureExecApprovals() {
260
+ const cfgPath = path3.join(os3.homedir(), ".openclaw", "exec-approvals.json");
261
+ if (!fs3.existsSync(cfgPath)) {
262
+ process.stdout.write(`\u26A0 ${cfgPath} not found \u2014 skipping exec-approvals config (OpenClaw may not be installed here).
263
+ `);
264
+ return;
265
+ }
266
+ let existing = {};
267
+ try {
268
+ existing = JSON.parse(fs3.readFileSync(cfgPath, "utf8"));
269
+ } catch {
270
+ }
271
+ const updated = {
272
+ ...existing,
273
+ defaults: {
274
+ security: "deny",
275
+ ask: "always",
276
+ askFallback: "deny",
277
+ autoAllowSkills: false
278
+ }
279
+ };
280
+ fs3.writeFileSync(cfgPath, JSON.stringify(updated, null, 2), "utf8");
281
+ process.stdout.write(`\u2713 exec-approvals.json configured \u2014 all webchat execs routed through FastGRC.
282
+ `);
283
+ }
180
284
  var [, , cmd, arg] = process.argv;
181
285
  if (cmd === "set-key") {
182
286
  if (!arg) {
@@ -271,10 +375,30 @@ if (cmd === "setup") {
271
375
  process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
272
376
  }
273
377
  doInstallHook(process.cwd());
274
- process.stdout.write("\n\u2713 Config and HOOK.md done.\n");
275
- process.stdout.write('Run "fastgrc-hook test" to verify the hook.\n');
378
+ doConfigureExecApprovals();
379
+ process.stdout.write("\n\u2713 Config, HOOK.md, and exec-approvals done.\n");
380
+ process.stdout.write('Restart OpenClaw, then run "fastgrc-hook test" to verify.\n');
276
381
  process.exit(0);
277
382
  }
383
+ if (cmd === "serve-approvals") {
384
+ const apiKey = resolveApiKey();
385
+ if (!apiKey) {
386
+ process.stderr.write("No API key configured. Run: fastgrc-hook set-key fgrc_k1_...\n");
387
+ process.exit(1);
388
+ }
389
+ const cfg = readExecApprovalsConfig();
390
+ if (!cfg?.socket?.path || !cfg?.socket?.token) {
391
+ process.stderr.write(
392
+ 'No exec-approvals socket configured.\nRun "fastgrc-hook setup" to configure OpenClaw exec-approvals, then restart OpenClaw.\n'
393
+ );
394
+ process.exit(1);
395
+ }
396
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
397
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
398
+ process.stdout.write(`[fastgrc] serve-approvals running \u2014 listening on ${cfg.socket.path}
399
+ `);
400
+ startExecApprovalsClient(apiKey, policyId, baseUrl);
401
+ }
278
402
  if (cmd === "uninstall") {
279
403
  const targetDir = arg || process.cwd();
280
404
  const cfg = readConfig();
@@ -341,7 +465,7 @@ if (cmd === "test") {
341
465
  `);
342
466
  process.exit(0);
343
467
  }
344
- if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook") {
468
+ if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook" || cmd === "serve-approvals") {
345
469
  } else {
346
470
  const apiKey = resolveApiKey();
347
471
  if (!apiKey) {
package/dist/plugin.d.mts CHANGED
@@ -1,3 +1,11 @@
1
+ interface ExecApprovalsConfig {
2
+ socket?: {
3
+ path?: string;
4
+ token?: string;
5
+ };
6
+ }
7
+ declare function readExecApprovalsConfig(): ExecApprovalsConfig | null;
8
+ declare function startExecApprovalsClient(apiKey: string, policyId?: string, baseUrl?: string): void;
1
9
  declare const pluginEntry: {
2
10
  id: string;
3
11
  name: string;
@@ -5,4 +13,4 @@ declare const pluginEntry: {
5
13
  register(api: any): void;
6
14
  };
7
15
 
8
- export { pluginEntry as default };
16
+ export { pluginEntry as default, readExecApprovalsConfig, startExecApprovalsClient };
package/dist/plugin.d.ts CHANGED
@@ -1,3 +1,11 @@
1
+ interface ExecApprovalsConfig {
2
+ socket?: {
3
+ path?: string;
4
+ token?: string;
5
+ };
6
+ }
7
+ declare function readExecApprovalsConfig(): ExecApprovalsConfig | null;
8
+ declare function startExecApprovalsClient(apiKey: string, policyId?: string, baseUrl?: string): void;
1
9
  declare const pluginEntry: {
2
10
  id: string;
3
11
  name: string;
@@ -5,4 +13,4 @@ declare const pluginEntry: {
5
13
  register(api: any): void;
6
14
  };
7
15
 
8
- export { pluginEntry as default };
16
+ export { pluginEntry as default, readExecApprovalsConfig, startExecApprovalsClient };
package/dist/plugin.js CHANGED
@@ -30,9 +30,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/plugin.ts
31
31
  var plugin_exports = {};
32
32
  __export(plugin_exports, {
33
- default: () => plugin_default
33
+ default: () => plugin_default,
34
+ readExecApprovalsConfig: () => readExecApprovalsConfig,
35
+ startExecApprovalsClient: () => startExecApprovalsClient
34
36
  });
35
37
  module.exports = __toCommonJS(plugin_exports);
38
+ var net = __toESM(require("net"));
39
+ var crypto = __toESM(require("crypto"));
40
+ var fs2 = __toESM(require("fs"));
41
+ var os2 = __toESM(require("os"));
42
+ var path2 = __toESM(require("path"));
36
43
 
37
44
  // src/index.ts
38
45
  var fs = __toESM(require("fs"));
@@ -114,6 +121,78 @@ args: ${JSON.stringify(args)}`;
114
121
  // src/plugin.ts
115
122
  var DEFAULT_BASE_URL2 = "https://app.fastgrc.ai";
116
123
  var DEFAULT_TIMEOUT_MS2 = 3e3;
124
+ function sign(payload, token) {
125
+ const nonce = crypto.randomBytes(16).toString("hex");
126
+ const ts = Math.floor(Date.now() / 1e3).toString();
127
+ const body = JSON.stringify(payload);
128
+ const mac = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${ts}:${body}`).digest("hex");
129
+ return { ...payload, nonce, timestamp: ts, hmac: mac };
130
+ }
131
+ function verifyHmac(req, token) {
132
+ try {
133
+ const { nonce, timestamp, hmac, ...rest } = req;
134
+ const expected = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${timestamp}:${JSON.stringify(rest)}`).digest("hex");
135
+ return hmac === expected;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+ function readExecApprovalsConfig() {
141
+ const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
142
+ try {
143
+ return JSON.parse(fs2.readFileSync(cfgPath, "utf8"));
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ function startExecApprovalsClient(apiKey, policyId, baseUrl) {
149
+ const cfg = readExecApprovalsConfig();
150
+ if (!cfg?.socket?.path || !cfg?.socket?.token) return;
151
+ const { path: sockPath, token } = cfg.socket;
152
+ function connect() {
153
+ const client = net.createConnection(sockPath);
154
+ let buffer = "";
155
+ client.on("connect", () => {
156
+ console.log("[fastgrc] exec-approvals socket connected");
157
+ });
158
+ client.on("data", async (buf) => {
159
+ buffer += buf.toString();
160
+ let req;
161
+ try {
162
+ req = JSON.parse(buffer);
163
+ } catch {
164
+ return;
165
+ }
166
+ buffer = "";
167
+ if (!verifyHmac(req, token)) {
168
+ console.warn("[fastgrc] exec-approvals: HMAC verification failed \u2014 ignoring request");
169
+ return;
170
+ }
171
+ let decision = "allow-once";
172
+ try {
173
+ const result = await evaluate({
174
+ toolName: "Exec",
175
+ args: { command: req.command, rawCommand: req.rawCommand, cwd: req.cwd },
176
+ agentId: req.agentId,
177
+ apiKey,
178
+ policyId,
179
+ baseUrl
180
+ });
181
+ if (result && (result.decision === "block" || result.decision === "require_approval")) {
182
+ decision = "deny";
183
+ const msg = result.policyContext?.matchedRule ? `[fastgrc] exec blocked: [${result.policyContext.matchedRule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`;
184
+ console.warn(msg);
185
+ }
186
+ } catch {
187
+ }
188
+ const response = sign({ approvalId: req.approvalId, decision }, token);
189
+ client.write(JSON.stringify(response));
190
+ });
191
+ client.on("error", () => setTimeout(connect, 5e3));
192
+ client.on("close", () => setTimeout(connect, 5e3));
193
+ }
194
+ connect();
195
+ }
117
196
  var pluginEntry = {
118
197
  id: "fastgrc",
119
198
  name: "FastGRC Policy Router",
@@ -128,31 +207,7 @@ var pluginEntry = {
128
207
  return;
129
208
  }
130
209
  console.log("[fastgrc] Plugin registered \u2014 before_tool_call hook active");
131
- const probeEvents = [
132
- "exec",
133
- "before_exec",
134
- "shell",
135
- "before_shell",
136
- "command",
137
- "before_command",
138
- "run",
139
- "before_run",
140
- "tool_call",
141
- "before_tool",
142
- "action",
143
- "before_action",
144
- "request",
145
- "before_request",
146
- "call",
147
- "before_call"
148
- ];
149
- for (const evt of probeEvents) {
150
- api.on(evt, (...args) => {
151
- console.log(`[fastgrc:hit] ${evt}`, JSON.stringify(args[0])?.slice(0, 120));
152
- });
153
- }
154
210
  api.on("before_tool_call", async (event, ctx) => {
155
- console.log(`[fastgrc] before_tool_call fired: ${event.toolName}`);
156
211
  const result = await evaluate({
157
212
  toolName: event.toolName,
158
213
  args: event.params,
@@ -182,7 +237,13 @@ var pluginEntry = {
182
237
  }
183
238
  return void 0;
184
239
  });
240
+ startExecApprovalsClient(apiKey, policyId, DEFAULT_BASE_URL2);
185
241
  }
186
242
  };
187
243
  var plugin_default = pluginEntry;
244
+ // Annotate the CommonJS export names for ESM import in node:
245
+ 0 && (module.exports = {
246
+ readExecApprovalsConfig,
247
+ startExecApprovalsClient
248
+ });
188
249
  module.exports = module.exports.default ?? module.exports;
package/dist/plugin.mjs CHANGED
@@ -1,3 +1,10 @@
1
+ // src/plugin.ts
2
+ import * as net from "net";
3
+ import * as crypto from "crypto";
4
+ import * as fs2 from "fs";
5
+ import * as os2 from "os";
6
+ import * as path2 from "path";
7
+
1
8
  // src/index.ts
2
9
  import * as fs from "fs";
3
10
  import * as os from "os";
@@ -78,6 +85,78 @@ args: ${JSON.stringify(args)}`;
78
85
  // src/plugin.ts
79
86
  var DEFAULT_BASE_URL2 = "https://app.fastgrc.ai";
80
87
  var DEFAULT_TIMEOUT_MS2 = 3e3;
88
+ function sign(payload, token) {
89
+ const nonce = crypto.randomBytes(16).toString("hex");
90
+ const ts = Math.floor(Date.now() / 1e3).toString();
91
+ const body = JSON.stringify(payload);
92
+ const mac = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${ts}:${body}`).digest("hex");
93
+ return { ...payload, nonce, timestamp: ts, hmac: mac };
94
+ }
95
+ function verifyHmac(req, token) {
96
+ try {
97
+ const { nonce, timestamp, hmac, ...rest } = req;
98
+ const expected = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${timestamp}:${JSON.stringify(rest)}`).digest("hex");
99
+ return hmac === expected;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+ function readExecApprovalsConfig() {
105
+ const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
106
+ try {
107
+ return JSON.parse(fs2.readFileSync(cfgPath, "utf8"));
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+ function startExecApprovalsClient(apiKey, policyId, baseUrl) {
113
+ const cfg = readExecApprovalsConfig();
114
+ if (!cfg?.socket?.path || !cfg?.socket?.token) return;
115
+ const { path: sockPath, token } = cfg.socket;
116
+ function connect() {
117
+ const client = net.createConnection(sockPath);
118
+ let buffer = "";
119
+ client.on("connect", () => {
120
+ console.log("[fastgrc] exec-approvals socket connected");
121
+ });
122
+ client.on("data", async (buf) => {
123
+ buffer += buf.toString();
124
+ let req;
125
+ try {
126
+ req = JSON.parse(buffer);
127
+ } catch {
128
+ return;
129
+ }
130
+ buffer = "";
131
+ if (!verifyHmac(req, token)) {
132
+ console.warn("[fastgrc] exec-approvals: HMAC verification failed \u2014 ignoring request");
133
+ return;
134
+ }
135
+ let decision = "allow-once";
136
+ try {
137
+ const result = await evaluate({
138
+ toolName: "Exec",
139
+ args: { command: req.command, rawCommand: req.rawCommand, cwd: req.cwd },
140
+ agentId: req.agentId,
141
+ apiKey,
142
+ policyId,
143
+ baseUrl
144
+ });
145
+ if (result && (result.decision === "block" || result.decision === "require_approval")) {
146
+ decision = "deny";
147
+ const msg = result.policyContext?.matchedRule ? `[fastgrc] exec blocked: [${result.policyContext.matchedRule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`;
148
+ console.warn(msg);
149
+ }
150
+ } catch {
151
+ }
152
+ const response = sign({ approvalId: req.approvalId, decision }, token);
153
+ client.write(JSON.stringify(response));
154
+ });
155
+ client.on("error", () => setTimeout(connect, 5e3));
156
+ client.on("close", () => setTimeout(connect, 5e3));
157
+ }
158
+ connect();
159
+ }
81
160
  var pluginEntry = {
82
161
  id: "fastgrc",
83
162
  name: "FastGRC Policy Router",
@@ -92,31 +171,7 @@ var pluginEntry = {
92
171
  return;
93
172
  }
94
173
  console.log("[fastgrc] Plugin registered \u2014 before_tool_call hook active");
95
- const probeEvents = [
96
- "exec",
97
- "before_exec",
98
- "shell",
99
- "before_shell",
100
- "command",
101
- "before_command",
102
- "run",
103
- "before_run",
104
- "tool_call",
105
- "before_tool",
106
- "action",
107
- "before_action",
108
- "request",
109
- "before_request",
110
- "call",
111
- "before_call"
112
- ];
113
- for (const evt of probeEvents) {
114
- api.on(evt, (...args) => {
115
- console.log(`[fastgrc:hit] ${evt}`, JSON.stringify(args[0])?.slice(0, 120));
116
- });
117
- }
118
174
  api.on("before_tool_call", async (event, ctx) => {
119
- console.log(`[fastgrc] before_tool_call fired: ${event.toolName}`);
120
175
  const result = await evaluate({
121
176
  toolName: event.toolName,
122
177
  args: event.params,
@@ -146,9 +201,12 @@ var pluginEntry = {
146
201
  }
147
202
  return void 0;
148
203
  });
204
+ startExecApprovalsClient(apiKey, policyId, DEFAULT_BASE_URL2);
149
205
  }
150
206
  };
151
207
  var plugin_default = pluginEntry;
152
208
  export {
153
- plugin_default as default
209
+ plugin_default as default,
210
+ readExecApprovalsConfig,
211
+ startExecApprovalsClient
154
212
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastgrc-openclaw",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
4
4
  "description": "FastGRC agent compliance plugin for OpenClaw — evaluates every tool call against your policy before it executes",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",