fastgrc-openclaw 1.0.28 → 1.0.30

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,107 @@ args: ${JSON.stringify(args)}`;
93
93
  }
94
94
  }
95
95
 
96
+ // src/plugin.ts
97
+ var net = __toESM(require("net"));
98
+ var fs2 = __toESM(require("fs"));
99
+ var os2 = __toESM(require("os"));
100
+ var path2 = __toESM(require("path"));
101
+ function readExecApprovalsConfig() {
102
+ const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
103
+ try {
104
+ return JSON.parse(fs2.readFileSync(cfgPath, "utf8"));
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ function startExecApprovalsServer(apiKey, policyId, baseUrl) {
110
+ const cfg = readExecApprovalsConfig();
111
+ if (!cfg?.socket?.path || !cfg?.socket?.token) {
112
+ console.warn("[fastgrc] exec-approvals: no socket config found \u2014 webchat execs will not be evaluated");
113
+ return;
114
+ }
115
+ const { path: sockPath, token: configToken } = cfg.socket;
116
+ try {
117
+ fs2.unlinkSync(sockPath);
118
+ } catch {
119
+ }
120
+ const server = net.createServer((conn) => {
121
+ let buffer = "";
122
+ conn.on("data", (chunk) => {
123
+ buffer += chunk.toString("utf8");
124
+ const idx = buffer.indexOf("\n");
125
+ if (idx === -1) return;
126
+ const line = buffer.slice(0, idx).trim();
127
+ buffer = "";
128
+ if (!line) {
129
+ conn.destroy();
130
+ return;
131
+ }
132
+ let msg;
133
+ try {
134
+ msg = JSON.parse(line);
135
+ } catch {
136
+ conn.destroy();
137
+ return;
138
+ }
139
+ if (msg.type !== "request" || msg.token !== configToken) {
140
+ conn.destroy();
141
+ return;
142
+ }
143
+ const req = msg.request ?? {};
144
+ const command = String(req.command ?? req.commandPreview ?? "unknown");
145
+ const agentId = req.agentId || void 0;
146
+ evaluate({
147
+ toolName: "Exec",
148
+ args: { command, cwd: req.cwd },
149
+ agentId,
150
+ apiKey,
151
+ policyId,
152
+ baseUrl
153
+ }).then((result) => {
154
+ let decision = "allow-once";
155
+ if (result && (result.decision === "block" || result.decision === "require_approval")) {
156
+ decision = "deny";
157
+ const rule = result.policyContext?.matchedRule;
158
+ console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
159
+ }
160
+ conn.write(JSON.stringify({ type: "decision", decision }) + "\n");
161
+ conn.destroy();
162
+ }).catch(() => {
163
+ conn.write(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
164
+ conn.destroy();
165
+ });
166
+ });
167
+ conn.on("error", () => {
168
+ });
169
+ });
170
+ server.listen(sockPath, () => {
171
+ try {
172
+ fs2.chmodSync(sockPath, 384);
173
+ } catch {
174
+ }
175
+ console.log(`[fastgrc] exec-approvals server listening on ${sockPath}`);
176
+ });
177
+ server.on("error", (err) => {
178
+ console.warn(`[fastgrc] exec-approvals server error: ${err.message}`);
179
+ });
180
+ }
181
+
96
182
  // src/bin.ts
97
- var CONFIG_PATH = path2.join(os2.homedir(), ".fastgrc.json");
183
+ var CONFIG_PATH = path3.join(os3.homedir(), ".fastgrc.json");
98
184
  function readConfig() {
99
185
  try {
100
- return JSON.parse(fs2.readFileSync(CONFIG_PATH, "utf8"));
186
+ return JSON.parse(fs3.readFileSync(CONFIG_PATH, "utf8"));
101
187
  } catch {
102
188
  return {};
103
189
  }
104
190
  }
105
191
  function writeConfig(data) {
106
- fs2.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
192
+ fs3.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
107
193
  }
108
194
  function computeHandler() {
109
195
  const binPath = process.argv[1];
110
- const homeDir = os2.homedir();
196
+ const homeDir = os3.homedir();
111
197
  return { binPath, homeDir, handlerStr: `HOME=${homeDir} node ${binPath}` };
112
198
  }
113
199
  function printTestSnippet(handlerStr) {
@@ -121,7 +207,7 @@ Test it directly (bypasses OpenClaw):
121
207
  );
122
208
  }
123
209
  function doInstallHook(targetDir) {
124
- const hookMdPath = path2.join(targetDir, "HOOK.md");
210
+ const hookMdPath = path3.join(targetDir, "HOOK.md");
125
211
  const { handlerStr } = computeHandler();
126
212
  const HOOK_ENTRY = ` - matcher: PreToolUse
127
213
  handler: "${handlerStr}"
@@ -131,8 +217,8 @@ function doInstallHook(targetDir) {
131
217
  if (!hasKey) {
132
218
  process.stdout.write("\u26A0 No API key set yet. Run: fastgrc-hook set-key fgrc_k1_your_key\n\n");
133
219
  }
134
- if (!fs2.existsSync(hookMdPath)) {
135
- fs2.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
220
+ if (!fs3.existsSync(hookMdPath)) {
221
+ fs3.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
136
222
  process.stdout.write(`\u2713 Created ${hookMdPath}
137
223
  Handler: ${handlerStr}
138
224
 
@@ -141,7 +227,7 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
141
227
  printTestSnippet(handlerStr);
142
228
  return;
143
229
  }
144
- const existing = fs2.readFileSync(hookMdPath, "utf8");
230
+ const existing = fs3.readFileSync(hookMdPath, "utf8");
145
231
  if (existing.includes(handlerStr)) {
146
232
  process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
147
233
  Handler: ${handlerStr}
@@ -154,7 +240,7 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
154
240
  /handler:\s*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"/,
155
241
  `handler: "${handlerStr}"`
156
242
  );
157
- fs2.writeFileSync(hookMdPath, patched, "utf8");
243
+ fs3.writeFileSync(hookMdPath, patched, "utf8");
158
244
  process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
159
245
  Handler: ${handlerStr}
160
246
 
@@ -173,7 +259,7 @@ ${HOOK_ENTRY}`;
173
259
  } else {
174
260
  updated = HOOK_BLOCK + "\n" + existing;
175
261
  }
176
- fs2.writeFileSync(hookMdPath, updated, "utf8");
262
+ fs3.writeFileSync(hookMdPath, updated, "utf8");
177
263
  process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
178
264
  Handler: ${handlerStr}
179
265
 
@@ -182,12 +268,12 @@ Restart OpenClaw to activate.
182
268
  printTestSnippet(handlerStr);
183
269
  }
184
270
  function doUninstallHook(targetDir) {
185
- const hookMdPath = path2.join(targetDir, "HOOK.md");
186
- if (!fs2.existsSync(hookMdPath)) {
271
+ const hookMdPath = path3.join(targetDir, "HOOK.md");
272
+ if (!fs3.existsSync(hookMdPath)) {
187
273
  process.stdout.write("No HOOK.md found \u2014 nothing to remove.\n");
188
274
  return;
189
275
  }
190
- const existing = fs2.readFileSync(hookMdPath, "utf8");
276
+ const existing = fs3.readFileSync(hookMdPath, "utf8");
191
277
  const patched = existing.replace(
192
278
  /[ \t]*-[ \t]*matcher:[ \t]*PreToolUse\n[ \t]*handler:[ \t]*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"\n?/,
193
279
  ""
@@ -195,11 +281,36 @@ function doUninstallHook(targetDir) {
195
281
  if (patched === existing) {
196
282
  process.stdout.write("FastGRC hook not found in HOOK.md \u2014 nothing to remove.\n");
197
283
  } else {
198
- fs2.writeFileSync(hookMdPath, patched, "utf8");
284
+ fs3.writeFileSync(hookMdPath, patched, "utf8");
199
285
  process.stdout.write(`\u2713 FastGRC hook removed from ${hookMdPath}
200
286
  `);
201
287
  }
202
288
  }
289
+ function doConfigureExecApprovals() {
290
+ const cfgPath = path3.join(os3.homedir(), ".openclaw", "exec-approvals.json");
291
+ if (!fs3.existsSync(cfgPath)) {
292
+ process.stdout.write(`\u26A0 ${cfgPath} not found \u2014 skipping exec-approvals config (OpenClaw may not be installed here).
293
+ `);
294
+ return;
295
+ }
296
+ let existing = {};
297
+ try {
298
+ existing = JSON.parse(fs3.readFileSync(cfgPath, "utf8"));
299
+ } catch {
300
+ }
301
+ const updated = {
302
+ ...existing,
303
+ defaults: {
304
+ security: "deny",
305
+ ask: "always",
306
+ askFallback: "deny",
307
+ autoAllowSkills: false
308
+ }
309
+ };
310
+ fs3.writeFileSync(cfgPath, JSON.stringify(updated, null, 2), "utf8");
311
+ process.stdout.write(`\u2713 exec-approvals.json configured \u2014 all webchat execs routed through FastGRC.
312
+ `);
313
+ }
203
314
  var [, , cmd, arg] = process.argv;
204
315
  if (cmd === "set-key") {
205
316
  if (!arg) {
@@ -294,10 +405,30 @@ if (cmd === "setup") {
294
405
  process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
295
406
  }
296
407
  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');
408
+ doConfigureExecApprovals();
409
+ process.stdout.write("\n\u2713 Config, HOOK.md, and exec-approvals done.\n");
410
+ process.stdout.write('Restart OpenClaw, then run "fastgrc-hook test" to verify.\n');
299
411
  process.exit(0);
300
412
  }
413
+ if (cmd === "serve-approvals") {
414
+ const apiKey = resolveApiKey();
415
+ if (!apiKey) {
416
+ process.stderr.write("No API key configured. Run: fastgrc-hook set-key fgrc_k1_...\n");
417
+ process.exit(1);
418
+ }
419
+ const cfg = readExecApprovalsConfig();
420
+ if (!cfg?.socket?.path || !cfg?.socket?.token) {
421
+ process.stderr.write(
422
+ 'No exec-approvals socket configured.\nRun "fastgrc-hook setup" to configure OpenClaw exec-approvals, then restart OpenClaw.\n'
423
+ );
424
+ process.exit(1);
425
+ }
426
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
427
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
428
+ process.stdout.write(`[fastgrc] serve-approvals running \u2014 listening on ${cfg.socket.path}
429
+ `);
430
+ startExecApprovalsServer(apiKey, policyId, baseUrl);
431
+ }
301
432
  if (cmd === "uninstall") {
302
433
  const targetDir = arg || process.cwd();
303
434
  const cfg = readConfig();
@@ -364,7 +495,7 @@ if (cmd === "test") {
364
495
  `);
365
496
  process.exit(0);
366
497
  }
367
- if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook") {
498
+ if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook" || cmd === "serve-approvals") {
368
499
  } else {
369
500
  const apiKey = resolveApiKey();
370
501
  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,107 @@ args: ${JSON.stringify(args)}`;
70
70
  }
71
71
  }
72
72
 
73
+ // src/plugin.ts
74
+ import * as net from "net";
75
+ import * as fs2 from "fs";
76
+ import * as os2 from "os";
77
+ import * as path2 from "path";
78
+ function readExecApprovalsConfig() {
79
+ const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
80
+ try {
81
+ return JSON.parse(fs2.readFileSync(cfgPath, "utf8"));
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ function startExecApprovalsServer(apiKey, policyId, baseUrl) {
87
+ const cfg = readExecApprovalsConfig();
88
+ if (!cfg?.socket?.path || !cfg?.socket?.token) {
89
+ console.warn("[fastgrc] exec-approvals: no socket config found \u2014 webchat execs will not be evaluated");
90
+ return;
91
+ }
92
+ const { path: sockPath, token: configToken } = cfg.socket;
93
+ try {
94
+ fs2.unlinkSync(sockPath);
95
+ } catch {
96
+ }
97
+ const server = net.createServer((conn) => {
98
+ let buffer = "";
99
+ conn.on("data", (chunk) => {
100
+ buffer += chunk.toString("utf8");
101
+ const idx = buffer.indexOf("\n");
102
+ if (idx === -1) return;
103
+ const line = buffer.slice(0, idx).trim();
104
+ buffer = "";
105
+ if (!line) {
106
+ conn.destroy();
107
+ return;
108
+ }
109
+ let msg;
110
+ try {
111
+ msg = JSON.parse(line);
112
+ } catch {
113
+ conn.destroy();
114
+ return;
115
+ }
116
+ if (msg.type !== "request" || msg.token !== configToken) {
117
+ conn.destroy();
118
+ return;
119
+ }
120
+ const req = msg.request ?? {};
121
+ const command = String(req.command ?? req.commandPreview ?? "unknown");
122
+ const agentId = req.agentId || void 0;
123
+ evaluate({
124
+ toolName: "Exec",
125
+ args: { command, cwd: req.cwd },
126
+ agentId,
127
+ apiKey,
128
+ policyId,
129
+ baseUrl
130
+ }).then((result) => {
131
+ let decision = "allow-once";
132
+ if (result && (result.decision === "block" || result.decision === "require_approval")) {
133
+ decision = "deny";
134
+ const rule = result.policyContext?.matchedRule;
135
+ console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
136
+ }
137
+ conn.write(JSON.stringify({ type: "decision", decision }) + "\n");
138
+ conn.destroy();
139
+ }).catch(() => {
140
+ conn.write(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
141
+ conn.destroy();
142
+ });
143
+ });
144
+ conn.on("error", () => {
145
+ });
146
+ });
147
+ server.listen(sockPath, () => {
148
+ try {
149
+ fs2.chmodSync(sockPath, 384);
150
+ } catch {
151
+ }
152
+ console.log(`[fastgrc] exec-approvals server listening on ${sockPath}`);
153
+ });
154
+ server.on("error", (err) => {
155
+ console.warn(`[fastgrc] exec-approvals server error: ${err.message}`);
156
+ });
157
+ }
158
+
73
159
  // src/bin.ts
74
- var CONFIG_PATH = path2.join(os2.homedir(), ".fastgrc.json");
160
+ var CONFIG_PATH = path3.join(os3.homedir(), ".fastgrc.json");
75
161
  function readConfig() {
76
162
  try {
77
- return JSON.parse(fs2.readFileSync(CONFIG_PATH, "utf8"));
163
+ return JSON.parse(fs3.readFileSync(CONFIG_PATH, "utf8"));
78
164
  } catch {
79
165
  return {};
80
166
  }
81
167
  }
82
168
  function writeConfig(data) {
83
- fs2.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
169
+ fs3.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
84
170
  }
85
171
  function computeHandler() {
86
172
  const binPath = process.argv[1];
87
- const homeDir = os2.homedir();
173
+ const homeDir = os3.homedir();
88
174
  return { binPath, homeDir, handlerStr: `HOME=${homeDir} node ${binPath}` };
89
175
  }
90
176
  function printTestSnippet(handlerStr) {
@@ -98,7 +184,7 @@ Test it directly (bypasses OpenClaw):
98
184
  );
99
185
  }
100
186
  function doInstallHook(targetDir) {
101
- const hookMdPath = path2.join(targetDir, "HOOK.md");
187
+ const hookMdPath = path3.join(targetDir, "HOOK.md");
102
188
  const { handlerStr } = computeHandler();
103
189
  const HOOK_ENTRY = ` - matcher: PreToolUse
104
190
  handler: "${handlerStr}"
@@ -108,8 +194,8 @@ function doInstallHook(targetDir) {
108
194
  if (!hasKey) {
109
195
  process.stdout.write("\u26A0 No API key set yet. Run: fastgrc-hook set-key fgrc_k1_your_key\n\n");
110
196
  }
111
- if (!fs2.existsSync(hookMdPath)) {
112
- fs2.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
197
+ if (!fs3.existsSync(hookMdPath)) {
198
+ fs3.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
113
199
  process.stdout.write(`\u2713 Created ${hookMdPath}
114
200
  Handler: ${handlerStr}
115
201
 
@@ -118,7 +204,7 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
118
204
  printTestSnippet(handlerStr);
119
205
  return;
120
206
  }
121
- const existing = fs2.readFileSync(hookMdPath, "utf8");
207
+ const existing = fs3.readFileSync(hookMdPath, "utf8");
122
208
  if (existing.includes(handlerStr)) {
123
209
  process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
124
210
  Handler: ${handlerStr}
@@ -131,7 +217,7 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
131
217
  /handler:\s*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"/,
132
218
  `handler: "${handlerStr}"`
133
219
  );
134
- fs2.writeFileSync(hookMdPath, patched, "utf8");
220
+ fs3.writeFileSync(hookMdPath, patched, "utf8");
135
221
  process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
136
222
  Handler: ${handlerStr}
137
223
 
@@ -150,7 +236,7 @@ ${HOOK_ENTRY}`;
150
236
  } else {
151
237
  updated = HOOK_BLOCK + "\n" + existing;
152
238
  }
153
- fs2.writeFileSync(hookMdPath, updated, "utf8");
239
+ fs3.writeFileSync(hookMdPath, updated, "utf8");
154
240
  process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
155
241
  Handler: ${handlerStr}
156
242
 
@@ -159,12 +245,12 @@ Restart OpenClaw to activate.
159
245
  printTestSnippet(handlerStr);
160
246
  }
161
247
  function doUninstallHook(targetDir) {
162
- const hookMdPath = path2.join(targetDir, "HOOK.md");
163
- if (!fs2.existsSync(hookMdPath)) {
248
+ const hookMdPath = path3.join(targetDir, "HOOK.md");
249
+ if (!fs3.existsSync(hookMdPath)) {
164
250
  process.stdout.write("No HOOK.md found \u2014 nothing to remove.\n");
165
251
  return;
166
252
  }
167
- const existing = fs2.readFileSync(hookMdPath, "utf8");
253
+ const existing = fs3.readFileSync(hookMdPath, "utf8");
168
254
  const patched = existing.replace(
169
255
  /[ \t]*-[ \t]*matcher:[ \t]*PreToolUse\n[ \t]*handler:[ \t]*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"\n?/,
170
256
  ""
@@ -172,11 +258,36 @@ function doUninstallHook(targetDir) {
172
258
  if (patched === existing) {
173
259
  process.stdout.write("FastGRC hook not found in HOOK.md \u2014 nothing to remove.\n");
174
260
  } else {
175
- fs2.writeFileSync(hookMdPath, patched, "utf8");
261
+ fs3.writeFileSync(hookMdPath, patched, "utf8");
176
262
  process.stdout.write(`\u2713 FastGRC hook removed from ${hookMdPath}
177
263
  `);
178
264
  }
179
265
  }
266
+ function doConfigureExecApprovals() {
267
+ const cfgPath = path3.join(os3.homedir(), ".openclaw", "exec-approvals.json");
268
+ if (!fs3.existsSync(cfgPath)) {
269
+ process.stdout.write(`\u26A0 ${cfgPath} not found \u2014 skipping exec-approvals config (OpenClaw may not be installed here).
270
+ `);
271
+ return;
272
+ }
273
+ let existing = {};
274
+ try {
275
+ existing = JSON.parse(fs3.readFileSync(cfgPath, "utf8"));
276
+ } catch {
277
+ }
278
+ const updated = {
279
+ ...existing,
280
+ defaults: {
281
+ security: "deny",
282
+ ask: "always",
283
+ askFallback: "deny",
284
+ autoAllowSkills: false
285
+ }
286
+ };
287
+ fs3.writeFileSync(cfgPath, JSON.stringify(updated, null, 2), "utf8");
288
+ process.stdout.write(`\u2713 exec-approvals.json configured \u2014 all webchat execs routed through FastGRC.
289
+ `);
290
+ }
180
291
  var [, , cmd, arg] = process.argv;
181
292
  if (cmd === "set-key") {
182
293
  if (!arg) {
@@ -271,10 +382,30 @@ if (cmd === "setup") {
271
382
  process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
272
383
  }
273
384
  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');
385
+ doConfigureExecApprovals();
386
+ process.stdout.write("\n\u2713 Config, HOOK.md, and exec-approvals done.\n");
387
+ process.stdout.write('Restart OpenClaw, then run "fastgrc-hook test" to verify.\n');
276
388
  process.exit(0);
277
389
  }
390
+ if (cmd === "serve-approvals") {
391
+ const apiKey = resolveApiKey();
392
+ if (!apiKey) {
393
+ process.stderr.write("No API key configured. Run: fastgrc-hook set-key fgrc_k1_...\n");
394
+ process.exit(1);
395
+ }
396
+ const cfg = readExecApprovalsConfig();
397
+ if (!cfg?.socket?.path || !cfg?.socket?.token) {
398
+ process.stderr.write(
399
+ 'No exec-approvals socket configured.\nRun "fastgrc-hook setup" to configure OpenClaw exec-approvals, then restart OpenClaw.\n'
400
+ );
401
+ process.exit(1);
402
+ }
403
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
404
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
405
+ process.stdout.write(`[fastgrc] serve-approvals running \u2014 listening on ${cfg.socket.path}
406
+ `);
407
+ startExecApprovalsServer(apiKey, policyId, baseUrl);
408
+ }
278
409
  if (cmd === "uninstall") {
279
410
  const targetDir = arg || process.cwd();
280
411
  const cfg = readConfig();
@@ -341,7 +472,7 @@ if (cmd === "test") {
341
472
  `);
342
473
  process.exit(0);
343
474
  }
344
- if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook") {
475
+ if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook" || cmd === "serve-approvals") {
345
476
  } else {
346
477
  const apiKey = resolveApiKey();
347
478
  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 startExecApprovalsServer(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, startExecApprovalsServer };
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 startExecApprovalsServer(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, startExecApprovalsServer };
package/dist/plugin.js CHANGED
@@ -30,9 +30,15 @@ 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
+ startExecApprovalsServer: () => startExecApprovalsServer
34
36
  });
35
37
  module.exports = __toCommonJS(plugin_exports);
38
+ var net = __toESM(require("net"));
39
+ var fs2 = __toESM(require("fs"));
40
+ var os2 = __toESM(require("os"));
41
+ var path2 = __toESM(require("path"));
36
42
 
37
43
  // src/index.ts
38
44
  var fs = __toESM(require("fs"));
@@ -114,6 +120,86 @@ args: ${JSON.stringify(args)}`;
114
120
  // src/plugin.ts
115
121
  var DEFAULT_BASE_URL2 = "https://app.fastgrc.ai";
116
122
  var DEFAULT_TIMEOUT_MS2 = 3e3;
123
+ function readExecApprovalsConfig() {
124
+ const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
125
+ try {
126
+ return JSON.parse(fs2.readFileSync(cfgPath, "utf8"));
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+ function startExecApprovalsServer(apiKey, policyId, baseUrl) {
132
+ const cfg = readExecApprovalsConfig();
133
+ if (!cfg?.socket?.path || !cfg?.socket?.token) {
134
+ console.warn("[fastgrc] exec-approvals: no socket config found \u2014 webchat execs will not be evaluated");
135
+ return;
136
+ }
137
+ const { path: sockPath, token: configToken } = cfg.socket;
138
+ try {
139
+ fs2.unlinkSync(sockPath);
140
+ } catch {
141
+ }
142
+ const server = net.createServer((conn) => {
143
+ let buffer = "";
144
+ conn.on("data", (chunk) => {
145
+ buffer += chunk.toString("utf8");
146
+ const idx = buffer.indexOf("\n");
147
+ if (idx === -1) return;
148
+ const line = buffer.slice(0, idx).trim();
149
+ buffer = "";
150
+ if (!line) {
151
+ conn.destroy();
152
+ return;
153
+ }
154
+ let msg;
155
+ try {
156
+ msg = JSON.parse(line);
157
+ } catch {
158
+ conn.destroy();
159
+ return;
160
+ }
161
+ if (msg.type !== "request" || msg.token !== configToken) {
162
+ conn.destroy();
163
+ return;
164
+ }
165
+ const req = msg.request ?? {};
166
+ const command = String(req.command ?? req.commandPreview ?? "unknown");
167
+ const agentId = req.agentId || void 0;
168
+ evaluate({
169
+ toolName: "Exec",
170
+ args: { command, cwd: req.cwd },
171
+ agentId,
172
+ apiKey,
173
+ policyId,
174
+ baseUrl
175
+ }).then((result) => {
176
+ let decision = "allow-once";
177
+ if (result && (result.decision === "block" || result.decision === "require_approval")) {
178
+ decision = "deny";
179
+ const rule = result.policyContext?.matchedRule;
180
+ console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
181
+ }
182
+ conn.write(JSON.stringify({ type: "decision", decision }) + "\n");
183
+ conn.destroy();
184
+ }).catch(() => {
185
+ conn.write(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
186
+ conn.destroy();
187
+ });
188
+ });
189
+ conn.on("error", () => {
190
+ });
191
+ });
192
+ server.listen(sockPath, () => {
193
+ try {
194
+ fs2.chmodSync(sockPath, 384);
195
+ } catch {
196
+ }
197
+ console.log(`[fastgrc] exec-approvals server listening on ${sockPath}`);
198
+ });
199
+ server.on("error", (err) => {
200
+ console.warn(`[fastgrc] exec-approvals server error: ${err.message}`);
201
+ });
202
+ }
117
203
  var pluginEntry = {
118
204
  id: "fastgrc",
119
205
  name: "FastGRC Policy Router",
@@ -128,31 +214,7 @@ var pluginEntry = {
128
214
  return;
129
215
  }
130
216
  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
217
  api.on("before_tool_call", async (event, ctx) => {
155
- console.log(`[fastgrc] before_tool_call fired: ${event.toolName}`);
156
218
  const result = await evaluate({
157
219
  toolName: event.toolName,
158
220
  args: event.params,
@@ -182,7 +244,13 @@ var pluginEntry = {
182
244
  }
183
245
  return void 0;
184
246
  });
247
+ startExecApprovalsServer(apiKey, policyId, DEFAULT_BASE_URL2);
185
248
  }
186
249
  };
187
250
  var plugin_default = pluginEntry;
251
+ // Annotate the CommonJS export names for ESM import in node:
252
+ 0 && (module.exports = {
253
+ readExecApprovalsConfig,
254
+ startExecApprovalsServer
255
+ });
188
256
  module.exports = module.exports.default ?? module.exports;
package/dist/plugin.mjs CHANGED
@@ -1,3 +1,9 @@
1
+ // src/plugin.ts
2
+ import * as net from "net";
3
+ import * as fs2 from "fs";
4
+ import * as os2 from "os";
5
+ import * as path2 from "path";
6
+
1
7
  // src/index.ts
2
8
  import * as fs from "fs";
3
9
  import * as os from "os";
@@ -78,6 +84,86 @@ args: ${JSON.stringify(args)}`;
78
84
  // src/plugin.ts
79
85
  var DEFAULT_BASE_URL2 = "https://app.fastgrc.ai";
80
86
  var DEFAULT_TIMEOUT_MS2 = 3e3;
87
+ function readExecApprovalsConfig() {
88
+ const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
89
+ try {
90
+ return JSON.parse(fs2.readFileSync(cfgPath, "utf8"));
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+ function startExecApprovalsServer(apiKey, policyId, baseUrl) {
96
+ const cfg = readExecApprovalsConfig();
97
+ if (!cfg?.socket?.path || !cfg?.socket?.token) {
98
+ console.warn("[fastgrc] exec-approvals: no socket config found \u2014 webchat execs will not be evaluated");
99
+ return;
100
+ }
101
+ const { path: sockPath, token: configToken } = cfg.socket;
102
+ try {
103
+ fs2.unlinkSync(sockPath);
104
+ } catch {
105
+ }
106
+ const server = net.createServer((conn) => {
107
+ let buffer = "";
108
+ conn.on("data", (chunk) => {
109
+ buffer += chunk.toString("utf8");
110
+ const idx = buffer.indexOf("\n");
111
+ if (idx === -1) return;
112
+ const line = buffer.slice(0, idx).trim();
113
+ buffer = "";
114
+ if (!line) {
115
+ conn.destroy();
116
+ return;
117
+ }
118
+ let msg;
119
+ try {
120
+ msg = JSON.parse(line);
121
+ } catch {
122
+ conn.destroy();
123
+ return;
124
+ }
125
+ if (msg.type !== "request" || msg.token !== configToken) {
126
+ conn.destroy();
127
+ return;
128
+ }
129
+ const req = msg.request ?? {};
130
+ const command = String(req.command ?? req.commandPreview ?? "unknown");
131
+ const agentId = req.agentId || void 0;
132
+ evaluate({
133
+ toolName: "Exec",
134
+ args: { command, cwd: req.cwd },
135
+ agentId,
136
+ apiKey,
137
+ policyId,
138
+ baseUrl
139
+ }).then((result) => {
140
+ let decision = "allow-once";
141
+ if (result && (result.decision === "block" || result.decision === "require_approval")) {
142
+ decision = "deny";
143
+ const rule = result.policyContext?.matchedRule;
144
+ console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
145
+ }
146
+ conn.write(JSON.stringify({ type: "decision", decision }) + "\n");
147
+ conn.destroy();
148
+ }).catch(() => {
149
+ conn.write(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
150
+ conn.destroy();
151
+ });
152
+ });
153
+ conn.on("error", () => {
154
+ });
155
+ });
156
+ server.listen(sockPath, () => {
157
+ try {
158
+ fs2.chmodSync(sockPath, 384);
159
+ } catch {
160
+ }
161
+ console.log(`[fastgrc] exec-approvals server listening on ${sockPath}`);
162
+ });
163
+ server.on("error", (err) => {
164
+ console.warn(`[fastgrc] exec-approvals server error: ${err.message}`);
165
+ });
166
+ }
81
167
  var pluginEntry = {
82
168
  id: "fastgrc",
83
169
  name: "FastGRC Policy Router",
@@ -92,31 +178,7 @@ var pluginEntry = {
92
178
  return;
93
179
  }
94
180
  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
181
  api.on("before_tool_call", async (event, ctx) => {
119
- console.log(`[fastgrc] before_tool_call fired: ${event.toolName}`);
120
182
  const result = await evaluate({
121
183
  toolName: event.toolName,
122
184
  args: event.params,
@@ -146,9 +208,12 @@ var pluginEntry = {
146
208
  }
147
209
  return void 0;
148
210
  });
211
+ startExecApprovalsServer(apiKey, policyId, DEFAULT_BASE_URL2);
149
212
  }
150
213
  };
151
214
  var plugin_default = pluginEntry;
152
215
  export {
153
- plugin_default as default
216
+ plugin_default as default,
217
+ readExecApprovalsConfig,
218
+ startExecApprovalsServer
154
219
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastgrc-openclaw",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
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",