fastgrc-openclaw 1.0.20 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/bin.js +253 -66
  2. package/dist/bin.mjs +259 -66
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -38,12 +38,12 @@ async function evaluate(payload) {
38
38
  agentId,
39
39
  agentType,
40
40
  agentName,
41
- apiKey: apiKey2,
42
- policyId: policyId2,
43
- baseUrl: baseUrl2 = DEFAULT_BASE_URL,
41
+ apiKey,
42
+ policyId,
43
+ baseUrl = DEFAULT_BASE_URL,
44
44
  timeoutMs = DEFAULT_TIMEOUT_MS
45
45
  } = payload;
46
- const evalUrl = `${baseUrl2.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
46
+ const evalUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
47
47
  const content = `tool_name: ${toolName}
48
48
  args: ${JSON.stringify(args)}`;
49
49
  try {
@@ -52,7 +52,7 @@ args: ${JSON.stringify(args)}`;
52
52
  const res = await fetch(evalUrl, {
53
53
  method: "POST",
54
54
  headers: {
55
- "Authorization": `Bearer ${apiKey2}`,
55
+ "Authorization": `Bearer ${apiKey}`,
56
56
  "Content-Type": "application/json"
57
57
  },
58
58
  body: JSON.stringify({
@@ -62,7 +62,7 @@ args: ${JSON.stringify(args)}`;
62
62
  agentId,
63
63
  agentType,
64
64
  agentName,
65
- ...policyId2 ? { policyId: policyId2 } : {}
65
+ ...policyId ? { policyId } : {}
66
66
  }),
67
67
  signal: controller.signal
68
68
  });
@@ -149,12 +149,47 @@ if (cmd === "unset-policy") {
149
149
  process.stdout.write("FastGRC policy ID removed \u2014 org-wide default will be used.\n");
150
150
  process.exit(0);
151
151
  }
152
+ function computeHandler() {
153
+ const binPath = process.argv[1];
154
+ const homeDir = os.homedir();
155
+ return { binPath, homeDir, handlerStr: `HOME=${homeDir} node ${binPath}` };
156
+ }
157
+ function printTestSnippet(handlerStr) {
158
+ process.stdout.write(
159
+ `
160
+ Test it directly (bypasses OpenClaw):
161
+ echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /production"},"session_id":"t1"}' \\
162
+ | ${handlerStr}
163
+ # exit 2 = blocked, exit 0 = allowed
164
+ `
165
+ );
166
+ }
167
+ if (cmd === "restart-openclaw") {
168
+ const { spawn, execSync } = require("child_process");
169
+ try {
170
+ execSync("pkill -x openclaw", { stdio: "ignore" });
171
+ process.stdout.write("Stopped existing openclaw process(es).\n");
172
+ } catch {
173
+ process.stdout.write("No openclaw process was running.\n");
174
+ }
175
+ const logPath = process.env.OPENCLAW_LOG ?? "/tmp/openclaw.log";
176
+ const workDir = arg || process.cwd();
177
+ const out = fs.openSync(logPath, "a");
178
+ const child = spawn("openclaw", [], {
179
+ detached: true,
180
+ stdio: ["ignore", out, out],
181
+ cwd: workDir
182
+ });
183
+ child.unref();
184
+ process.stdout.write(`\u2713 OpenClaw started (PID: ${child.pid}, cwd: ${workDir})
185
+ Logs: tail -f ${logPath}
186
+ `);
187
+ process.exit(0);
188
+ }
152
189
  if (cmd === "install-hook") {
153
190
  const targetDir = arg || process.cwd();
154
191
  const hookMdPath = path.join(targetDir, "HOOK.md");
155
- const binPath = process.argv[1];
156
- const homeDir = os.homedir();
157
- const handlerStr = `HOME=${homeDir} node ${binPath}`;
192
+ const { handlerStr } = computeHandler();
158
193
  const HOOK_ENTRY = ` - matcher: PreToolUse
159
194
  handler: "${handlerStr}"
160
195
  `;
@@ -166,15 +201,19 @@ if (cmd === "install-hook") {
166
201
  if (!fs.existsSync(hookMdPath)) {
167
202
  fs.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
168
203
  process.stdout.write(`\u2713 Created ${hookMdPath}
204
+ Handler: ${handlerStr}
169
205
 
170
206
  Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
171
207
  `);
208
+ printTestSnippet(handlerStr);
172
209
  process.exit(0);
173
210
  }
174
211
  const existing = fs.readFileSync(hookMdPath, "utf8");
175
212
  if (existing.includes(handlerStr)) {
176
213
  process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
214
+ Handler: ${handlerStr}
177
215
  `);
216
+ printTestSnippet(handlerStr);
178
217
  process.exit(0);
179
218
  }
180
219
  if (existing.includes("fastgrc-hook") || existing.includes("fastgrc-openclaw")) {
@@ -184,9 +223,11 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
184
223
  );
185
224
  fs.writeFileSync(hookMdPath, patched, "utf8");
186
225
  process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
226
+ Handler: ${handlerStr}
187
227
 
188
228
  Restart OpenClaw to activate.
189
229
  `);
230
+ printTestSnippet(handlerStr);
190
231
  process.exit(0);
191
232
  }
192
233
  const fmEnd = existing.indexOf("\n---", 3);
@@ -201,82 +242,228 @@ ${HOOK_ENTRY}`;
201
242
  }
202
243
  fs.writeFileSync(hookMdPath, updated, "utf8");
203
244
  process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
245
+ Handler: ${handlerStr}
204
246
 
205
247
  Restart OpenClaw to activate.
206
248
  `);
249
+ printTestSnippet(handlerStr);
207
250
  process.exit(0);
208
251
  }
209
- var apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
210
- if (!apiKey) {
211
- process.stderr.write("[fastgrc-hook] No API key configured \u2014 run: fastgrc-hook set-key fgrc_k1_...\n");
212
- process.exit(0);
213
- }
214
- var baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
215
- var policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
216
- async function main() {
217
- const chunks = [];
218
- for await (const chunk of process.stdin) chunks.push(chunk);
219
- const raw = Buffer.concat(chunks).toString("utf8").trim();
220
- const toolName = process.env.HOOK_TOOL_NAME ?? "";
221
- const toolInputRaw = process.env.HOOK_TOOL_INPUT ?? "{}";
222
- let args = {};
223
- let agentId;
224
- if (raw) {
225
- try {
226
- const ctx = JSON.parse(raw);
227
- args = ctx.tool_input ?? ctx.input ?? ctx.args ?? {};
228
- agentId = ctx.agent_id ?? ctx.agentId ?? ctx.session_id;
229
- } catch {
252
+ if (cmd === "setup") {
253
+ const { execSync } = require("child_process");
254
+ const rest = process.argv.slice(3);
255
+ let apiKeyArg;
256
+ let policyIdArg;
257
+ for (let i = 0; i < rest.length; i++) {
258
+ if (rest[i] === "--api-key" && rest[i + 1]) {
259
+ apiKeyArg = rest[++i];
260
+ } else if (rest[i] === "--policy-id" && rest[i + 1]) {
261
+ policyIdArg = rest[++i];
262
+ } else if (!apiKeyArg && !rest[i].startsWith("--")) {
263
+ apiKeyArg = rest[i];
264
+ } else if (!policyIdArg && !rest[i].startsWith("--")) {
265
+ policyIdArg = rest[i];
230
266
  }
231
267
  }
232
- if (!toolName && !raw) {
268
+ if (!apiKeyArg) {
269
+ process.stderr.write("Usage: fastgrc-hook setup --api-key <key> [--policy-id <id>]\n");
270
+ process.exit(1);
271
+ }
272
+ const selfBin = process.argv[1];
273
+ process.stdout.write("Step 1/4: Installing OpenClaw plugin as root...\n");
274
+ try {
275
+ execSync("sudo openclaw plugins remove fastgrc-openclaw", { stdio: "ignore" });
276
+ } catch {
277
+ }
278
+ execSync("sudo openclaw plugins install fastgrc-openclaw", { stdio: "inherit" });
279
+ writeConfig({ ...readConfig(), apiKey: apiKeyArg });
280
+ process.stdout.write("Step 2/4: API key saved to ~/.fastgrc.json\n");
281
+ if (policyIdArg) {
282
+ writeConfig({ ...readConfig(), policyId: policyIdArg });
283
+ process.stdout.write("Step 3/4: Policy ID saved to ~/.fastgrc.json\n");
284
+ } else {
285
+ process.stdout.write("Step 3/4: No policy ID provided \u2014 org-wide default will be used.\n");
286
+ }
287
+ process.stdout.write("Step 4/4: Wiring up HOOK.md and restarting OpenClaw...\n");
288
+ execSync(`node "${selfBin}" install-hook`, { stdio: "inherit" });
289
+ execSync(`node "${selfBin}" restart-openclaw`, { stdio: "inherit" });
290
+ process.stdout.write('\n\u2713 FastGRC setup complete. Run "fastgrc-hook test" to verify.\n');
291
+ process.exit(0);
292
+ }
293
+ if (cmd === "uninstall-hook") {
294
+ const targetDir = arg || process.cwd();
295
+ const hookMdPath = path.join(targetDir, "HOOK.md");
296
+ if (!fs.existsSync(hookMdPath)) {
297
+ process.stdout.write("No HOOK.md found \u2014 nothing to remove.\n");
233
298
  process.exit(0);
234
299
  }
235
- const resolvedToolName = toolName || (raw ? JSON.parse(raw)?.tool_name ?? "unknown" : "unknown");
236
- if (!args || Object.keys(args).length === 0) {
237
- try {
238
- args = JSON.parse(toolInputRaw);
239
- } catch {
240
- args = {};
241
- }
300
+ const existing = fs.readFileSync(hookMdPath, "utf8");
301
+ const patched = existing.replace(
302
+ /[ \t]*-[ \t]*matcher:[ \t]*PreToolUse\n[ \t]*handler:[ \t]*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"\n?/,
303
+ ""
304
+ );
305
+ if (patched === existing) {
306
+ process.stdout.write("FastGRC hook not found in HOOK.md \u2014 nothing to remove.\n");
307
+ } else {
308
+ fs.writeFileSync(hookMdPath, patched, "utf8");
309
+ process.stdout.write(`\u2713 FastGRC hook removed from ${hookMdPath}
310
+ `);
242
311
  }
243
- const result = await evaluate({
244
- toolName: resolvedToolName,
245
- args,
246
- agentId,
312
+ process.exit(0);
313
+ }
314
+ if (cmd === "uninstall") {
315
+ const { execSync } = require("child_process");
316
+ const selfBin = process.argv[1];
317
+ const targetDir = arg || process.cwd();
318
+ process.stdout.write("Removing FastGRC from this OpenClaw deployment...\n\n");
319
+ process.stdout.write("1. Removing OpenClaw plugin...\n");
320
+ try {
321
+ execSync("sudo openclaw plugins remove fastgrc-openclaw", { stdio: "inherit" });
322
+ } catch {
323
+ process.stdout.write(" (plugin was not installed \u2014 skipped)\n");
324
+ }
325
+ process.stdout.write("2. Clearing config...\n");
326
+ const cfg = readConfig();
327
+ delete cfg.apiKey;
328
+ delete cfg.policyId;
329
+ writeConfig(cfg);
330
+ process.stdout.write(" API key and policy ID removed from ~/.fastgrc.json\n");
331
+ process.stdout.write("3. Removing hook from HOOK.md...\n");
332
+ execSync(`node "${selfBin}" uninstall-hook "${targetDir}"`, { stdio: "inherit" });
333
+ process.stdout.write("4. Restarting OpenClaw...\n");
334
+ execSync(`node "${selfBin}" restart-openclaw`, { stdio: "inherit" });
335
+ process.stdout.write(
336
+ "\n\u2713 FastGRC removed.\nTo fully remove the CLI: npm uninstall -g fastgrc-openclaw\n"
337
+ );
338
+ process.exit(0);
339
+ }
340
+ if (cmd === "test") {
341
+ const testCommand = arg || "rm -rf /production-db";
342
+ const payload = JSON.stringify({
343
+ tool_name: "Bash",
344
+ tool_input: { command: testCommand },
345
+ session_id: "fastgrc-hook-test"
346
+ });
347
+ const { handlerStr } = computeHandler();
348
+ process.stdout.write(`Testing handler: ${handlerStr}
349
+ `);
350
+ process.stdout.write(`Payload: ${payload}
351
+
352
+ `);
353
+ const apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
354
+ if (!apiKey) {
355
+ process.stderr.write("No API key configured. Run: fastgrc-hook set-key fgrc_k1_...\n");
356
+ process.exit(1);
357
+ }
358
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
359
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
360
+ const ctx = JSON.parse(payload);
361
+ evaluate({
362
+ toolName: ctx.tool_name,
363
+ args: ctx.tool_input,
364
+ agentId: ctx.session_id,
247
365
  apiKey,
248
- // narrowed at module level via early-exit guard above
249
366
  policyId,
250
367
  baseUrl
368
+ }).then((result) => {
369
+ if (!result) {
370
+ process.stdout.write("Evaluate returned null (fail-open) \u2014 network error or timeout.\n");
371
+ process.exit(0);
372
+ }
373
+ process.stdout.write(`Decision: ${result.decision}
374
+ `);
375
+ if (result.reasoning) process.stdout.write(`Reason: ${result.reasoning}
376
+ `);
377
+ if (result.policyContext?.matchedRule) process.stdout.write(`Matched rule: ${result.policyContext.matchedRule}
378
+ `);
379
+ process.exit(result.decision === "block" || result.decision === "require_approval" ? 2 : 0);
380
+ }).catch((err) => {
381
+ process.stderr.write(`Test failed: ${err}
382
+ `);
383
+ process.exit(1);
251
384
  });
252
- if (!result) {
385
+ } else if (cmd === "where" || cmd === "which-hook") {
386
+ const { handlerStr, binPath } = computeHandler();
387
+ process.stdout.write(`Binary: ${binPath}
388
+ `);
389
+ process.stdout.write(`Handler: ${handlerStr}
390
+ `);
391
+ process.exit(0);
392
+ }
393
+ if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "restart-openclaw" || cmd === "setup" || cmd === "uninstall" || cmd === "uninstall-hook") {
394
+ } else {
395
+ const apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
396
+ if (!apiKey) {
397
+ process.stderr.write("[fastgrc-hook] No API key configured \u2014 run: fastgrc-hook set-key fgrc_k1_...\n");
253
398
  process.exit(0);
254
399
  }
255
- const { decision, reasoning, policyContext, reasonTags } = result;
256
- const matchedRule = policyContext?.matchedRule;
257
- if (decision === "block") {
258
- const msg = matchedRule ? `FastGRC blocked: [${matchedRule}] ${reasoning}` : `FastGRC blocked: ${reasoning}`;
259
- process.stderr.write(msg + "\n");
260
- process.exit(2);
261
- }
262
- if (decision === "require_approval") {
263
- process.stderr.write(
264
- `FastGRC requires approval before this action.
400
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
401
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
402
+ async function main() {
403
+ const chunks = [];
404
+ for await (const chunk of process.stdin) chunks.push(chunk);
405
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
406
+ const toolName = process.env.HOOK_TOOL_NAME ?? "";
407
+ const toolInputRaw = process.env.HOOK_TOOL_INPUT ?? "{}";
408
+ let args = {};
409
+ let agentId;
410
+ if (raw) {
411
+ try {
412
+ const ctx = JSON.parse(raw);
413
+ args = ctx.tool_input ?? ctx.input ?? ctx.args ?? {};
414
+ agentId = ctx.agent_id ?? ctx.agentId ?? ctx.session_id;
415
+ } catch {
416
+ }
417
+ }
418
+ if (!toolName && !raw) {
419
+ process.exit(0);
420
+ }
421
+ const resolvedToolName = toolName || (raw ? JSON.parse(raw)?.tool_name ?? "unknown" : "unknown");
422
+ if (!args || Object.keys(args).length === 0) {
423
+ try {
424
+ args = JSON.parse(toolInputRaw);
425
+ } catch {
426
+ args = {};
427
+ }
428
+ }
429
+ const result = await evaluate({
430
+ toolName: resolvedToolName,
431
+ args,
432
+ agentId,
433
+ apiKey,
434
+ // narrowed at module level via early-exit guard above
435
+ policyId,
436
+ baseUrl
437
+ });
438
+ if (!result) {
439
+ process.exit(0);
440
+ }
441
+ const { decision, reasoning, policyContext, reasonTags } = result;
442
+ const matchedRule = policyContext?.matchedRule;
443
+ if (decision === "block") {
444
+ const msg = matchedRule ? `FastGRC blocked: [${matchedRule}] ${reasoning}` : `FastGRC blocked: ${reasoning}`;
445
+ process.stderr.write(msg + "\n");
446
+ process.exit(2);
447
+ }
448
+ if (decision === "require_approval") {
449
+ process.stderr.write(
450
+ `FastGRC requires approval before this action.
265
451
  ${reasoning}
266
452
  Review at: ${baseUrl}/dashboard/agent-policies
267
453
  `
268
- );
269
- process.exit(2);
270
- }
271
- if (Array.isArray(reasonTags) && reasonTags.includes("override_block_active")) {
272
- const msg = matchedRule ? `[FastGRC] Observability mode \u2014 would have blocked: [${matchedRule}] ${reasoning}` : `[FastGRC] Observability mode \u2014 would have blocked: ${reasoning}`;
273
- process.stderr.write(msg + "\n");
454
+ );
455
+ process.exit(2);
456
+ }
457
+ if (Array.isArray(reasonTags) && reasonTags.includes("override_block_active")) {
458
+ const msg = matchedRule ? `[FastGRC] Observability mode \u2014 would have blocked: [${matchedRule}] ${reasoning}` : `[FastGRC] Observability mode \u2014 would have blocked: ${reasoning}`;
459
+ process.stderr.write(msg + "\n");
460
+ }
461
+ process.exit(0);
274
462
  }
275
- process.exit(0);
276
- }
277
- main().catch((err) => {
278
- process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
463
+ main().catch((err) => {
464
+ process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
279
465
  `);
280
- process.exit(0);
281
- });
466
+ process.exit(0);
467
+ });
468
+ }
282
469
  module.exports = module.exports.default ?? module.exports;
package/dist/bin.mjs CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/bin.ts
4
10
  import * as fs from "fs";
@@ -15,12 +21,12 @@ async function evaluate(payload) {
15
21
  agentId,
16
22
  agentType,
17
23
  agentName,
18
- apiKey: apiKey2,
19
- policyId: policyId2,
20
- baseUrl: baseUrl2 = DEFAULT_BASE_URL,
24
+ apiKey,
25
+ policyId,
26
+ baseUrl = DEFAULT_BASE_URL,
21
27
  timeoutMs = DEFAULT_TIMEOUT_MS
22
28
  } = payload;
23
- const evalUrl = `${baseUrl2.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
29
+ const evalUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
24
30
  const content = `tool_name: ${toolName}
25
31
  args: ${JSON.stringify(args)}`;
26
32
  try {
@@ -29,7 +35,7 @@ args: ${JSON.stringify(args)}`;
29
35
  const res = await fetch(evalUrl, {
30
36
  method: "POST",
31
37
  headers: {
32
- "Authorization": `Bearer ${apiKey2}`,
38
+ "Authorization": `Bearer ${apiKey}`,
33
39
  "Content-Type": "application/json"
34
40
  },
35
41
  body: JSON.stringify({
@@ -39,7 +45,7 @@ args: ${JSON.stringify(args)}`;
39
45
  agentId,
40
46
  agentType,
41
47
  agentName,
42
- ...policyId2 ? { policyId: policyId2 } : {}
48
+ ...policyId ? { policyId } : {}
43
49
  }),
44
50
  signal: controller.signal
45
51
  });
@@ -126,12 +132,47 @@ if (cmd === "unset-policy") {
126
132
  process.stdout.write("FastGRC policy ID removed \u2014 org-wide default will be used.\n");
127
133
  process.exit(0);
128
134
  }
135
+ function computeHandler() {
136
+ const binPath = process.argv[1];
137
+ const homeDir = os.homedir();
138
+ return { binPath, homeDir, handlerStr: `HOME=${homeDir} node ${binPath}` };
139
+ }
140
+ function printTestSnippet(handlerStr) {
141
+ process.stdout.write(
142
+ `
143
+ Test it directly (bypasses OpenClaw):
144
+ echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /production"},"session_id":"t1"}' \\
145
+ | ${handlerStr}
146
+ # exit 2 = blocked, exit 0 = allowed
147
+ `
148
+ );
149
+ }
150
+ if (cmd === "restart-openclaw") {
151
+ const { spawn, execSync } = __require("child_process");
152
+ try {
153
+ execSync("pkill -x openclaw", { stdio: "ignore" });
154
+ process.stdout.write("Stopped existing openclaw process(es).\n");
155
+ } catch {
156
+ process.stdout.write("No openclaw process was running.\n");
157
+ }
158
+ const logPath = process.env.OPENCLAW_LOG ?? "/tmp/openclaw.log";
159
+ const workDir = arg || process.cwd();
160
+ const out = fs.openSync(logPath, "a");
161
+ const child = spawn("openclaw", [], {
162
+ detached: true,
163
+ stdio: ["ignore", out, out],
164
+ cwd: workDir
165
+ });
166
+ child.unref();
167
+ process.stdout.write(`\u2713 OpenClaw started (PID: ${child.pid}, cwd: ${workDir})
168
+ Logs: tail -f ${logPath}
169
+ `);
170
+ process.exit(0);
171
+ }
129
172
  if (cmd === "install-hook") {
130
173
  const targetDir = arg || process.cwd();
131
174
  const hookMdPath = path.join(targetDir, "HOOK.md");
132
- const binPath = process.argv[1];
133
- const homeDir = os.homedir();
134
- const handlerStr = `HOME=${homeDir} node ${binPath}`;
175
+ const { handlerStr } = computeHandler();
135
176
  const HOOK_ENTRY = ` - matcher: PreToolUse
136
177
  handler: "${handlerStr}"
137
178
  `;
@@ -143,15 +184,19 @@ if (cmd === "install-hook") {
143
184
  if (!fs.existsSync(hookMdPath)) {
144
185
  fs.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
145
186
  process.stdout.write(`\u2713 Created ${hookMdPath}
187
+ Handler: ${handlerStr}
146
188
 
147
189
  Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
148
190
  `);
191
+ printTestSnippet(handlerStr);
149
192
  process.exit(0);
150
193
  }
151
194
  const existing = fs.readFileSync(hookMdPath, "utf8");
152
195
  if (existing.includes(handlerStr)) {
153
196
  process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
197
+ Handler: ${handlerStr}
154
198
  `);
199
+ printTestSnippet(handlerStr);
155
200
  process.exit(0);
156
201
  }
157
202
  if (existing.includes("fastgrc-hook") || existing.includes("fastgrc-openclaw")) {
@@ -161,9 +206,11 @@ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
161
206
  );
162
207
  fs.writeFileSync(hookMdPath, patched, "utf8");
163
208
  process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
209
+ Handler: ${handlerStr}
164
210
 
165
211
  Restart OpenClaw to activate.
166
212
  `);
213
+ printTestSnippet(handlerStr);
167
214
  process.exit(0);
168
215
  }
169
216
  const fmEnd = existing.indexOf("\n---", 3);
@@ -178,81 +225,227 @@ ${HOOK_ENTRY}`;
178
225
  }
179
226
  fs.writeFileSync(hookMdPath, updated, "utf8");
180
227
  process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
228
+ Handler: ${handlerStr}
181
229
 
182
230
  Restart OpenClaw to activate.
183
231
  `);
232
+ printTestSnippet(handlerStr);
184
233
  process.exit(0);
185
234
  }
186
- var apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
187
- if (!apiKey) {
188
- process.stderr.write("[fastgrc-hook] No API key configured \u2014 run: fastgrc-hook set-key fgrc_k1_...\n");
189
- process.exit(0);
190
- }
191
- var baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
192
- var policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
193
- async function main() {
194
- const chunks = [];
195
- for await (const chunk of process.stdin) chunks.push(chunk);
196
- const raw = Buffer.concat(chunks).toString("utf8").trim();
197
- const toolName = process.env.HOOK_TOOL_NAME ?? "";
198
- const toolInputRaw = process.env.HOOK_TOOL_INPUT ?? "{}";
199
- let args = {};
200
- let agentId;
201
- if (raw) {
202
- try {
203
- const ctx = JSON.parse(raw);
204
- args = ctx.tool_input ?? ctx.input ?? ctx.args ?? {};
205
- agentId = ctx.agent_id ?? ctx.agentId ?? ctx.session_id;
206
- } catch {
235
+ if (cmd === "setup") {
236
+ const { execSync } = __require("child_process");
237
+ const rest = process.argv.slice(3);
238
+ let apiKeyArg;
239
+ let policyIdArg;
240
+ for (let i = 0; i < rest.length; i++) {
241
+ if (rest[i] === "--api-key" && rest[i + 1]) {
242
+ apiKeyArg = rest[++i];
243
+ } else if (rest[i] === "--policy-id" && rest[i + 1]) {
244
+ policyIdArg = rest[++i];
245
+ } else if (!apiKeyArg && !rest[i].startsWith("--")) {
246
+ apiKeyArg = rest[i];
247
+ } else if (!policyIdArg && !rest[i].startsWith("--")) {
248
+ policyIdArg = rest[i];
207
249
  }
208
250
  }
209
- if (!toolName && !raw) {
251
+ if (!apiKeyArg) {
252
+ process.stderr.write("Usage: fastgrc-hook setup --api-key <key> [--policy-id <id>]\n");
253
+ process.exit(1);
254
+ }
255
+ const selfBin = process.argv[1];
256
+ process.stdout.write("Step 1/4: Installing OpenClaw plugin as root...\n");
257
+ try {
258
+ execSync("sudo openclaw plugins remove fastgrc-openclaw", { stdio: "ignore" });
259
+ } catch {
260
+ }
261
+ execSync("sudo openclaw plugins install fastgrc-openclaw", { stdio: "inherit" });
262
+ writeConfig({ ...readConfig(), apiKey: apiKeyArg });
263
+ process.stdout.write("Step 2/4: API key saved to ~/.fastgrc.json\n");
264
+ if (policyIdArg) {
265
+ writeConfig({ ...readConfig(), policyId: policyIdArg });
266
+ process.stdout.write("Step 3/4: Policy ID saved to ~/.fastgrc.json\n");
267
+ } else {
268
+ process.stdout.write("Step 3/4: No policy ID provided \u2014 org-wide default will be used.\n");
269
+ }
270
+ process.stdout.write("Step 4/4: Wiring up HOOK.md and restarting OpenClaw...\n");
271
+ execSync(`node "${selfBin}" install-hook`, { stdio: "inherit" });
272
+ execSync(`node "${selfBin}" restart-openclaw`, { stdio: "inherit" });
273
+ process.stdout.write('\n\u2713 FastGRC setup complete. Run "fastgrc-hook test" to verify.\n');
274
+ process.exit(0);
275
+ }
276
+ if (cmd === "uninstall-hook") {
277
+ const targetDir = arg || process.cwd();
278
+ const hookMdPath = path.join(targetDir, "HOOK.md");
279
+ if (!fs.existsSync(hookMdPath)) {
280
+ process.stdout.write("No HOOK.md found \u2014 nothing to remove.\n");
210
281
  process.exit(0);
211
282
  }
212
- const resolvedToolName = toolName || (raw ? JSON.parse(raw)?.tool_name ?? "unknown" : "unknown");
213
- if (!args || Object.keys(args).length === 0) {
214
- try {
215
- args = JSON.parse(toolInputRaw);
216
- } catch {
217
- args = {};
218
- }
283
+ const existing = fs.readFileSync(hookMdPath, "utf8");
284
+ const patched = existing.replace(
285
+ /[ \t]*-[ \t]*matcher:[ \t]*PreToolUse\n[ \t]*handler:[ \t]*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"\n?/,
286
+ ""
287
+ );
288
+ if (patched === existing) {
289
+ process.stdout.write("FastGRC hook not found in HOOK.md \u2014 nothing to remove.\n");
290
+ } else {
291
+ fs.writeFileSync(hookMdPath, patched, "utf8");
292
+ process.stdout.write(`\u2713 FastGRC hook removed from ${hookMdPath}
293
+ `);
219
294
  }
220
- const result = await evaluate({
221
- toolName: resolvedToolName,
222
- args,
223
- agentId,
295
+ process.exit(0);
296
+ }
297
+ if (cmd === "uninstall") {
298
+ const { execSync } = __require("child_process");
299
+ const selfBin = process.argv[1];
300
+ const targetDir = arg || process.cwd();
301
+ process.stdout.write("Removing FastGRC from this OpenClaw deployment...\n\n");
302
+ process.stdout.write("1. Removing OpenClaw plugin...\n");
303
+ try {
304
+ execSync("sudo openclaw plugins remove fastgrc-openclaw", { stdio: "inherit" });
305
+ } catch {
306
+ process.stdout.write(" (plugin was not installed \u2014 skipped)\n");
307
+ }
308
+ process.stdout.write("2. Clearing config...\n");
309
+ const cfg = readConfig();
310
+ delete cfg.apiKey;
311
+ delete cfg.policyId;
312
+ writeConfig(cfg);
313
+ process.stdout.write(" API key and policy ID removed from ~/.fastgrc.json\n");
314
+ process.stdout.write("3. Removing hook from HOOK.md...\n");
315
+ execSync(`node "${selfBin}" uninstall-hook "${targetDir}"`, { stdio: "inherit" });
316
+ process.stdout.write("4. Restarting OpenClaw...\n");
317
+ execSync(`node "${selfBin}" restart-openclaw`, { stdio: "inherit" });
318
+ process.stdout.write(
319
+ "\n\u2713 FastGRC removed.\nTo fully remove the CLI: npm uninstall -g fastgrc-openclaw\n"
320
+ );
321
+ process.exit(0);
322
+ }
323
+ if (cmd === "test") {
324
+ const testCommand = arg || "rm -rf /production-db";
325
+ const payload = JSON.stringify({
326
+ tool_name: "Bash",
327
+ tool_input: { command: testCommand },
328
+ session_id: "fastgrc-hook-test"
329
+ });
330
+ const { handlerStr } = computeHandler();
331
+ process.stdout.write(`Testing handler: ${handlerStr}
332
+ `);
333
+ process.stdout.write(`Payload: ${payload}
334
+
335
+ `);
336
+ const apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
337
+ if (!apiKey) {
338
+ process.stderr.write("No API key configured. Run: fastgrc-hook set-key fgrc_k1_...\n");
339
+ process.exit(1);
340
+ }
341
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
342
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
343
+ const ctx = JSON.parse(payload);
344
+ evaluate({
345
+ toolName: ctx.tool_name,
346
+ args: ctx.tool_input,
347
+ agentId: ctx.session_id,
224
348
  apiKey,
225
- // narrowed at module level via early-exit guard above
226
349
  policyId,
227
350
  baseUrl
351
+ }).then((result) => {
352
+ if (!result) {
353
+ process.stdout.write("Evaluate returned null (fail-open) \u2014 network error or timeout.\n");
354
+ process.exit(0);
355
+ }
356
+ process.stdout.write(`Decision: ${result.decision}
357
+ `);
358
+ if (result.reasoning) process.stdout.write(`Reason: ${result.reasoning}
359
+ `);
360
+ if (result.policyContext?.matchedRule) process.stdout.write(`Matched rule: ${result.policyContext.matchedRule}
361
+ `);
362
+ process.exit(result.decision === "block" || result.decision === "require_approval" ? 2 : 0);
363
+ }).catch((err) => {
364
+ process.stderr.write(`Test failed: ${err}
365
+ `);
366
+ process.exit(1);
228
367
  });
229
- if (!result) {
368
+ } else if (cmd === "where" || cmd === "which-hook") {
369
+ const { handlerStr, binPath } = computeHandler();
370
+ process.stdout.write(`Binary: ${binPath}
371
+ `);
372
+ process.stdout.write(`Handler: ${handlerStr}
373
+ `);
374
+ process.exit(0);
375
+ }
376
+ if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "restart-openclaw" || cmd === "setup" || cmd === "uninstall" || cmd === "uninstall-hook") {
377
+ } else {
378
+ const apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
379
+ if (!apiKey) {
380
+ process.stderr.write("[fastgrc-hook] No API key configured \u2014 run: fastgrc-hook set-key fgrc_k1_...\n");
230
381
  process.exit(0);
231
382
  }
232
- const { decision, reasoning, policyContext, reasonTags } = result;
233
- const matchedRule = policyContext?.matchedRule;
234
- if (decision === "block") {
235
- const msg = matchedRule ? `FastGRC blocked: [${matchedRule}] ${reasoning}` : `FastGRC blocked: ${reasoning}`;
236
- process.stderr.write(msg + "\n");
237
- process.exit(2);
238
- }
239
- if (decision === "require_approval") {
240
- process.stderr.write(
241
- `FastGRC requires approval before this action.
383
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
384
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
385
+ async function main() {
386
+ const chunks = [];
387
+ for await (const chunk of process.stdin) chunks.push(chunk);
388
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
389
+ const toolName = process.env.HOOK_TOOL_NAME ?? "";
390
+ const toolInputRaw = process.env.HOOK_TOOL_INPUT ?? "{}";
391
+ let args = {};
392
+ let agentId;
393
+ if (raw) {
394
+ try {
395
+ const ctx = JSON.parse(raw);
396
+ args = ctx.tool_input ?? ctx.input ?? ctx.args ?? {};
397
+ agentId = ctx.agent_id ?? ctx.agentId ?? ctx.session_id;
398
+ } catch {
399
+ }
400
+ }
401
+ if (!toolName && !raw) {
402
+ process.exit(0);
403
+ }
404
+ const resolvedToolName = toolName || (raw ? JSON.parse(raw)?.tool_name ?? "unknown" : "unknown");
405
+ if (!args || Object.keys(args).length === 0) {
406
+ try {
407
+ args = JSON.parse(toolInputRaw);
408
+ } catch {
409
+ args = {};
410
+ }
411
+ }
412
+ const result = await evaluate({
413
+ toolName: resolvedToolName,
414
+ args,
415
+ agentId,
416
+ apiKey,
417
+ // narrowed at module level via early-exit guard above
418
+ policyId,
419
+ baseUrl
420
+ });
421
+ if (!result) {
422
+ process.exit(0);
423
+ }
424
+ const { decision, reasoning, policyContext, reasonTags } = result;
425
+ const matchedRule = policyContext?.matchedRule;
426
+ if (decision === "block") {
427
+ const msg = matchedRule ? `FastGRC blocked: [${matchedRule}] ${reasoning}` : `FastGRC blocked: ${reasoning}`;
428
+ process.stderr.write(msg + "\n");
429
+ process.exit(2);
430
+ }
431
+ if (decision === "require_approval") {
432
+ process.stderr.write(
433
+ `FastGRC requires approval before this action.
242
434
  ${reasoning}
243
435
  Review at: ${baseUrl}/dashboard/agent-policies
244
436
  `
245
- );
246
- process.exit(2);
247
- }
248
- if (Array.isArray(reasonTags) && reasonTags.includes("override_block_active")) {
249
- const msg = matchedRule ? `[FastGRC] Observability mode \u2014 would have blocked: [${matchedRule}] ${reasoning}` : `[FastGRC] Observability mode \u2014 would have blocked: ${reasoning}`;
250
- process.stderr.write(msg + "\n");
437
+ );
438
+ process.exit(2);
439
+ }
440
+ if (Array.isArray(reasonTags) && reasonTags.includes("override_block_active")) {
441
+ const msg = matchedRule ? `[FastGRC] Observability mode \u2014 would have blocked: [${matchedRule}] ${reasoning}` : `[FastGRC] Observability mode \u2014 would have blocked: ${reasoning}`;
442
+ process.stderr.write(msg + "\n");
443
+ }
444
+ process.exit(0);
251
445
  }
252
- process.exit(0);
253
- }
254
- main().catch((err) => {
255
- process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
446
+ main().catch((err) => {
447
+ process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
256
448
  `);
257
- process.exit(0);
258
- });
449
+ process.exit(0);
450
+ });
451
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastgrc-openclaw",
3
- "version": "1.0.20",
3
+ "version": "1.0.23",
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",