fastgrc-openclaw 1.0.20 → 1.0.24

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 +261 -115
  2. package/dist/bin.mjs +261 -115
  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
  });
@@ -91,6 +91,101 @@ function readConfig() {
91
91
  function writeConfig(data) {
92
92
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
93
93
  }
94
+ function computeHandler() {
95
+ const binPath = process.argv[1];
96
+ const homeDir = os.homedir();
97
+ return { binPath, homeDir, handlerStr: `HOME=${homeDir} node ${binPath}` };
98
+ }
99
+ function printTestSnippet(handlerStr) {
100
+ process.stdout.write(
101
+ `
102
+ Test it directly (bypasses OpenClaw):
103
+ echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /production"},"session_id":"t1"}' \\
104
+ | ${handlerStr}
105
+ # exit 2 = blocked, exit 0 = allowed
106
+ `
107
+ );
108
+ }
109
+ function doInstallHook(targetDir) {
110
+ const hookMdPath = path.join(targetDir, "HOOK.md");
111
+ const { handlerStr } = computeHandler();
112
+ const HOOK_ENTRY = ` - matcher: PreToolUse
113
+ handler: "${handlerStr}"
114
+ `;
115
+ const HOOK_BLOCK = "---\nname: FastGRC Policy Check\ndescription: Evaluate every tool call against your FastGRC compliance policy\nhooks:\n" + HOOK_ENTRY + "---\n";
116
+ const hasKey = !!(readConfig().apiKey || process.env.FASTGRC_API_KEY);
117
+ if (!hasKey) {
118
+ process.stdout.write("\u26A0 No API key set yet. Run: fastgrc-hook set-key fgrc_k1_your_key\n\n");
119
+ }
120
+ if (!fs.existsSync(hookMdPath)) {
121
+ fs.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
122
+ process.stdout.write(`\u2713 Created ${hookMdPath}
123
+ Handler: ${handlerStr}
124
+
125
+ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
126
+ `);
127
+ printTestSnippet(handlerStr);
128
+ return;
129
+ }
130
+ const existing = fs.readFileSync(hookMdPath, "utf8");
131
+ if (existing.includes(handlerStr)) {
132
+ process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
133
+ Handler: ${handlerStr}
134
+ `);
135
+ printTestSnippet(handlerStr);
136
+ return;
137
+ }
138
+ if (existing.includes("fastgrc-hook") || existing.includes("fastgrc-openclaw")) {
139
+ const patched = existing.replace(
140
+ /handler:\s*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"/,
141
+ `handler: "${handlerStr}"`
142
+ );
143
+ fs.writeFileSync(hookMdPath, patched, "utf8");
144
+ process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
145
+ Handler: ${handlerStr}
146
+
147
+ Restart OpenClaw to activate.
148
+ `);
149
+ printTestSnippet(handlerStr);
150
+ return;
151
+ }
152
+ const fmEnd = existing.indexOf("\n---", 3);
153
+ let updated;
154
+ if (fmEnd !== -1) {
155
+ const hasHooksKey = existing.lastIndexOf("hooks:", fmEnd) !== -1;
156
+ const insertText = hasHooksKey ? HOOK_ENTRY : `hooks:
157
+ ${HOOK_ENTRY}`;
158
+ updated = existing.slice(0, fmEnd) + "\n" + insertText + existing.slice(fmEnd);
159
+ } else {
160
+ updated = HOOK_BLOCK + "\n" + existing;
161
+ }
162
+ fs.writeFileSync(hookMdPath, updated, "utf8");
163
+ process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
164
+ Handler: ${handlerStr}
165
+
166
+ Restart OpenClaw to activate.
167
+ `);
168
+ printTestSnippet(handlerStr);
169
+ }
170
+ function doUninstallHook(targetDir) {
171
+ const hookMdPath = path.join(targetDir, "HOOK.md");
172
+ if (!fs.existsSync(hookMdPath)) {
173
+ process.stdout.write("No HOOK.md found \u2014 nothing to remove.\n");
174
+ return;
175
+ }
176
+ const existing = fs.readFileSync(hookMdPath, "utf8");
177
+ const patched = existing.replace(
178
+ /[ \t]*-[ \t]*matcher:[ \t]*PreToolUse\n[ \t]*handler:[ \t]*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"\n?/,
179
+ ""
180
+ );
181
+ if (patched === existing) {
182
+ process.stdout.write("FastGRC hook not found in HOOK.md \u2014 nothing to remove.\n");
183
+ } else {
184
+ fs.writeFileSync(hookMdPath, patched, "utf8");
185
+ process.stdout.write(`\u2713 FastGRC hook removed from ${hookMdPath}
186
+ `);
187
+ }
188
+ }
94
189
  var [, , cmd, arg] = process.argv;
95
190
  if (cmd === "set-key") {
96
191
  if (!arg) {
@@ -150,133 +245,184 @@ if (cmd === "unset-policy") {
150
245
  process.exit(0);
151
246
  }
152
247
  if (cmd === "install-hook") {
153
- const targetDir = arg || process.cwd();
154
- 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}`;
158
- const HOOK_ENTRY = ` - matcher: PreToolUse
159
- handler: "${handlerStr}"
160
- `;
161
- const HOOK_BLOCK = "---\nname: FastGRC Policy Check\ndescription: Evaluate every tool call against your FastGRC compliance policy\nhooks:\n" + HOOK_ENTRY + "---\n";
162
- const hasKey = !!(readConfig().apiKey || process.env.FASTGRC_API_KEY);
163
- if (!hasKey) {
164
- process.stdout.write("\u26A0 No API key set yet. Run: fastgrc-hook set-key fgrc_k1_your_key\n\n");
165
- }
166
- if (!fs.existsSync(hookMdPath)) {
167
- fs.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
168
- process.stdout.write(`\u2713 Created ${hookMdPath}
169
-
170
- Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
171
- `);
172
- process.exit(0);
173
- }
174
- const existing = fs.readFileSync(hookMdPath, "utf8");
175
- if (existing.includes(handlerStr)) {
176
- process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
177
- `);
178
- process.exit(0);
248
+ doInstallHook(arg || process.cwd());
249
+ process.exit(0);
250
+ }
251
+ if (cmd === "uninstall-hook") {
252
+ doUninstallHook(arg || process.cwd());
253
+ process.exit(0);
254
+ }
255
+ if (cmd === "setup") {
256
+ const rest = process.argv.slice(3);
257
+ let apiKeyArg;
258
+ let policyIdArg;
259
+ for (let i = 0; i < rest.length; i++) {
260
+ if (rest[i] === "--api-key" && rest[i + 1]) {
261
+ apiKeyArg = rest[++i];
262
+ } else if (rest[i] === "--policy-id" && rest[i + 1]) {
263
+ policyIdArg = rest[++i];
264
+ } else if (!apiKeyArg && !rest[i].startsWith("--")) {
265
+ apiKeyArg = rest[i];
266
+ } else if (!policyIdArg && !rest[i].startsWith("--")) {
267
+ policyIdArg = rest[i];
268
+ }
179
269
  }
180
- if (existing.includes("fastgrc-hook") || existing.includes("fastgrc-openclaw")) {
181
- const patched = existing.replace(
182
- /handler:\s*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"/,
183
- `handler: "${handlerStr}"`
184
- );
185
- fs.writeFileSync(hookMdPath, patched, "utf8");
186
- process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
187
-
188
- Restart OpenClaw to activate.
189
- `);
190
- process.exit(0);
270
+ if (!apiKeyArg) {
271
+ process.stderr.write("Usage: fastgrc-hook setup --api-key <key> [--policy-id <id>]\n");
272
+ process.exit(1);
191
273
  }
192
- const fmEnd = existing.indexOf("\n---", 3);
193
- let updated;
194
- if (fmEnd !== -1) {
195
- const hasHooksKey = existing.lastIndexOf("hooks:", fmEnd) !== -1;
196
- const insertText = hasHooksKey ? HOOK_ENTRY : `hooks:
197
- ${HOOK_ENTRY}`;
198
- updated = existing.slice(0, fmEnd) + "\n" + insertText + existing.slice(fmEnd);
274
+ writeConfig({ ...readConfig(), apiKey: apiKeyArg });
275
+ process.stdout.write("\u2713 API key saved to ~/.fastgrc.json\n");
276
+ if (policyIdArg) {
277
+ writeConfig({ ...readConfig(), policyId: policyIdArg });
278
+ process.stdout.write("\u2713 Policy ID saved to ~/.fastgrc.json\n");
199
279
  } else {
200
- updated = HOOK_BLOCK + "\n" + existing;
280
+ process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
201
281
  }
202
- fs.writeFileSync(hookMdPath, updated, "utf8");
203
- process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
204
-
205
- Restart OpenClaw to activate.
206
- `);
282
+ doInstallHook(process.cwd());
283
+ process.stdout.write("\n\u2713 Config and HOOK.md done.\n");
284
+ process.stdout.write('Run "fastgrc-hook test" to verify the hook.\n');
207
285
  process.exit(0);
208
286
  }
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");
287
+ if (cmd === "uninstall") {
288
+ const targetDir = arg || process.cwd();
289
+ const cfg = readConfig();
290
+ delete cfg.apiKey;
291
+ delete cfg.policyId;
292
+ writeConfig(cfg);
293
+ process.stdout.write("\u2713 API key and policy ID removed from ~/.fastgrc.json\n");
294
+ doUninstallHook(targetDir);
295
+ process.stdout.write(
296
+ "\n\u2713 Config and HOOK.md cleaned up.\nComplete removal \u2014 run these two commands:\n sudo openclaw plugins remove fastgrc-openclaw\n npm uninstall -g fastgrc-openclaw\n"
297
+ );
212
298
  process.exit(0);
213
299
  }
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 {
230
- }
231
- }
232
- if (!toolName && !raw) {
233
- process.exit(0);
234
- }
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
+ if (cmd === "test") {
301
+ const testCommand = arg || "rm -rf /production-db";
302
+ const payload = JSON.stringify({
303
+ tool_name: "Bash",
304
+ tool_input: { command: testCommand },
305
+ session_id: "fastgrc-hook-test"
306
+ });
307
+ const { handlerStr } = computeHandler();
308
+ process.stdout.write(`Testing handler: ${handlerStr}
309
+ `);
310
+ process.stdout.write(`Payload: ${payload}
311
+
312
+ `);
313
+ const apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
314
+ if (!apiKey) {
315
+ process.stderr.write("No API key configured. Run: fastgrc-hook set-key fgrc_k1_...\n");
316
+ process.exit(1);
242
317
  }
243
- const result = await evaluate({
244
- toolName: resolvedToolName,
245
- args,
246
- agentId,
318
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
319
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
320
+ const ctx = JSON.parse(payload);
321
+ evaluate({
322
+ toolName: ctx.tool_name,
323
+ args: ctx.tool_input,
324
+ agentId: ctx.session_id,
247
325
  apiKey,
248
- // narrowed at module level via early-exit guard above
249
326
  policyId,
250
327
  baseUrl
328
+ }).then((result) => {
329
+ if (!result) {
330
+ process.stdout.write("Evaluate returned null (fail-open) \u2014 network error or timeout.\n");
331
+ process.exit(0);
332
+ }
333
+ process.stdout.write(`Decision: ${result.decision}
334
+ `);
335
+ if (result.reasoning) process.stdout.write(`Reason: ${result.reasoning}
336
+ `);
337
+ if (result.policyContext?.matchedRule) process.stdout.write(`Matched rule: ${result.policyContext.matchedRule}
338
+ `);
339
+ process.exit(result.decision === "block" || result.decision === "require_approval" ? 2 : 0);
340
+ }).catch((err) => {
341
+ process.stderr.write(`Test failed: ${err}
342
+ `);
343
+ process.exit(1);
251
344
  });
252
- if (!result) {
345
+ } else if (cmd === "where" || cmd === "which-hook") {
346
+ const { handlerStr, binPath } = computeHandler();
347
+ process.stdout.write(`Binary: ${binPath}
348
+ `);
349
+ process.stdout.write(`Handler: ${handlerStr}
350
+ `);
351
+ process.exit(0);
352
+ }
353
+ if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook") {
354
+ } else {
355
+ const apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
356
+ if (!apiKey) {
357
+ process.stderr.write("[fastgrc-hook] No API key configured \u2014 run: fastgrc-hook set-key fgrc_k1_...\n");
253
358
  process.exit(0);
254
359
  }
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.
360
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
361
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
362
+ async function main() {
363
+ const chunks = [];
364
+ for await (const chunk of process.stdin) chunks.push(chunk);
365
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
366
+ const toolName = process.env.HOOK_TOOL_NAME ?? "";
367
+ const toolInputRaw = process.env.HOOK_TOOL_INPUT ?? "{}";
368
+ let args = {};
369
+ let agentId;
370
+ if (raw) {
371
+ try {
372
+ const ctx = JSON.parse(raw);
373
+ args = ctx.tool_input ?? ctx.input ?? ctx.args ?? {};
374
+ agentId = ctx.agent_id ?? ctx.agentId ?? ctx.session_id;
375
+ } catch {
376
+ }
377
+ }
378
+ if (!toolName && !raw) {
379
+ process.exit(0);
380
+ }
381
+ const resolvedToolName = toolName || (raw ? JSON.parse(raw)?.tool_name ?? "unknown" : "unknown");
382
+ if (!args || Object.keys(args).length === 0) {
383
+ try {
384
+ args = JSON.parse(toolInputRaw);
385
+ } catch {
386
+ args = {};
387
+ }
388
+ }
389
+ const result = await evaluate({
390
+ toolName: resolvedToolName,
391
+ args,
392
+ agentId,
393
+ apiKey,
394
+ policyId,
395
+ baseUrl
396
+ });
397
+ if (!result) {
398
+ process.exit(0);
399
+ }
400
+ const { decision, reasoning, policyContext, reasonTags } = result;
401
+ const matchedRule = policyContext?.matchedRule;
402
+ if (decision === "block") {
403
+ const msg = matchedRule ? `FastGRC blocked: [${matchedRule}] ${reasoning}` : `FastGRC blocked: ${reasoning}`;
404
+ process.stderr.write(msg + "\n");
405
+ process.exit(2);
406
+ }
407
+ if (decision === "require_approval") {
408
+ process.stderr.write(
409
+ `FastGRC requires approval before this action.
265
410
  ${reasoning}
266
411
  Review at: ${baseUrl}/dashboard/agent-policies
267
412
  `
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");
413
+ );
414
+ process.exit(2);
415
+ }
416
+ if (Array.isArray(reasonTags) && reasonTags.includes("override_block_active")) {
417
+ const msg = matchedRule ? `[FastGRC] Observability mode \u2014 would have blocked: [${matchedRule}] ${reasoning}` : `[FastGRC] Observability mode \u2014 would have blocked: ${reasoning}`;
418
+ process.stderr.write(msg + "\n");
419
+ }
420
+ process.exit(0);
274
421
  }
275
- process.exit(0);
276
- }
277
- main().catch((err) => {
278
- process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
422
+ main().catch((err) => {
423
+ process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
279
424
  `);
280
- process.exit(0);
281
- });
425
+ process.exit(0);
426
+ });
427
+ }
282
428
  module.exports = module.exports.default ?? module.exports;
package/dist/bin.mjs CHANGED
@@ -15,12 +15,12 @@ async function evaluate(payload) {
15
15
  agentId,
16
16
  agentType,
17
17
  agentName,
18
- apiKey: apiKey2,
19
- policyId: policyId2,
20
- baseUrl: baseUrl2 = DEFAULT_BASE_URL,
18
+ apiKey,
19
+ policyId,
20
+ baseUrl = DEFAULT_BASE_URL,
21
21
  timeoutMs = DEFAULT_TIMEOUT_MS
22
22
  } = payload;
23
- const evalUrl = `${baseUrl2.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
23
+ const evalUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
24
24
  const content = `tool_name: ${toolName}
25
25
  args: ${JSON.stringify(args)}`;
26
26
  try {
@@ -29,7 +29,7 @@ args: ${JSON.stringify(args)}`;
29
29
  const res = await fetch(evalUrl, {
30
30
  method: "POST",
31
31
  headers: {
32
- "Authorization": `Bearer ${apiKey2}`,
32
+ "Authorization": `Bearer ${apiKey}`,
33
33
  "Content-Type": "application/json"
34
34
  },
35
35
  body: JSON.stringify({
@@ -39,7 +39,7 @@ args: ${JSON.stringify(args)}`;
39
39
  agentId,
40
40
  agentType,
41
41
  agentName,
42
- ...policyId2 ? { policyId: policyId2 } : {}
42
+ ...policyId ? { policyId } : {}
43
43
  }),
44
44
  signal: controller.signal
45
45
  });
@@ -68,6 +68,101 @@ function readConfig() {
68
68
  function writeConfig(data) {
69
69
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 384 });
70
70
  }
71
+ function computeHandler() {
72
+ const binPath = process.argv[1];
73
+ const homeDir = os.homedir();
74
+ return { binPath, homeDir, handlerStr: `HOME=${homeDir} node ${binPath}` };
75
+ }
76
+ function printTestSnippet(handlerStr) {
77
+ process.stdout.write(
78
+ `
79
+ Test it directly (bypasses OpenClaw):
80
+ echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /production"},"session_id":"t1"}' \\
81
+ | ${handlerStr}
82
+ # exit 2 = blocked, exit 0 = allowed
83
+ `
84
+ );
85
+ }
86
+ function doInstallHook(targetDir) {
87
+ const hookMdPath = path.join(targetDir, "HOOK.md");
88
+ const { handlerStr } = computeHandler();
89
+ const HOOK_ENTRY = ` - matcher: PreToolUse
90
+ handler: "${handlerStr}"
91
+ `;
92
+ const HOOK_BLOCK = "---\nname: FastGRC Policy Check\ndescription: Evaluate every tool call against your FastGRC compliance policy\nhooks:\n" + HOOK_ENTRY + "---\n";
93
+ const hasKey = !!(readConfig().apiKey || process.env.FASTGRC_API_KEY);
94
+ if (!hasKey) {
95
+ process.stdout.write("\u26A0 No API key set yet. Run: fastgrc-hook set-key fgrc_k1_your_key\n\n");
96
+ }
97
+ if (!fs.existsSync(hookMdPath)) {
98
+ fs.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
99
+ process.stdout.write(`\u2713 Created ${hookMdPath}
100
+ Handler: ${handlerStr}
101
+
102
+ Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
103
+ `);
104
+ printTestSnippet(handlerStr);
105
+ return;
106
+ }
107
+ const existing = fs.readFileSync(hookMdPath, "utf8");
108
+ if (existing.includes(handlerStr)) {
109
+ process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
110
+ Handler: ${handlerStr}
111
+ `);
112
+ printTestSnippet(handlerStr);
113
+ return;
114
+ }
115
+ if (existing.includes("fastgrc-hook") || existing.includes("fastgrc-openclaw")) {
116
+ const patched = existing.replace(
117
+ /handler:\s*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"/,
118
+ `handler: "${handlerStr}"`
119
+ );
120
+ fs.writeFileSync(hookMdPath, patched, "utf8");
121
+ process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
122
+ Handler: ${handlerStr}
123
+
124
+ Restart OpenClaw to activate.
125
+ `);
126
+ printTestSnippet(handlerStr);
127
+ return;
128
+ }
129
+ const fmEnd = existing.indexOf("\n---", 3);
130
+ let updated;
131
+ if (fmEnd !== -1) {
132
+ const hasHooksKey = existing.lastIndexOf("hooks:", fmEnd) !== -1;
133
+ const insertText = hasHooksKey ? HOOK_ENTRY : `hooks:
134
+ ${HOOK_ENTRY}`;
135
+ updated = existing.slice(0, fmEnd) + "\n" + insertText + existing.slice(fmEnd);
136
+ } else {
137
+ updated = HOOK_BLOCK + "\n" + existing;
138
+ }
139
+ fs.writeFileSync(hookMdPath, updated, "utf8");
140
+ process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
141
+ Handler: ${handlerStr}
142
+
143
+ Restart OpenClaw to activate.
144
+ `);
145
+ printTestSnippet(handlerStr);
146
+ }
147
+ function doUninstallHook(targetDir) {
148
+ const hookMdPath = path.join(targetDir, "HOOK.md");
149
+ if (!fs.existsSync(hookMdPath)) {
150
+ process.stdout.write("No HOOK.md found \u2014 nothing to remove.\n");
151
+ return;
152
+ }
153
+ const existing = fs.readFileSync(hookMdPath, "utf8");
154
+ const patched = existing.replace(
155
+ /[ \t]*-[ \t]*matcher:[ \t]*PreToolUse\n[ \t]*handler:[ \t]*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"\n?/,
156
+ ""
157
+ );
158
+ if (patched === existing) {
159
+ process.stdout.write("FastGRC hook not found in HOOK.md \u2014 nothing to remove.\n");
160
+ } else {
161
+ fs.writeFileSync(hookMdPath, patched, "utf8");
162
+ process.stdout.write(`\u2713 FastGRC hook removed from ${hookMdPath}
163
+ `);
164
+ }
165
+ }
71
166
  var [, , cmd, arg] = process.argv;
72
167
  if (cmd === "set-key") {
73
168
  if (!arg) {
@@ -127,132 +222,183 @@ if (cmd === "unset-policy") {
127
222
  process.exit(0);
128
223
  }
129
224
  if (cmd === "install-hook") {
130
- const targetDir = arg || process.cwd();
131
- 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}`;
135
- const HOOK_ENTRY = ` - matcher: PreToolUse
136
- handler: "${handlerStr}"
137
- `;
138
- const HOOK_BLOCK = "---\nname: FastGRC Policy Check\ndescription: Evaluate every tool call against your FastGRC compliance policy\nhooks:\n" + HOOK_ENTRY + "---\n";
139
- const hasKey = !!(readConfig().apiKey || process.env.FASTGRC_API_KEY);
140
- if (!hasKey) {
141
- process.stdout.write("\u26A0 No API key set yet. Run: fastgrc-hook set-key fgrc_k1_your_key\n\n");
142
- }
143
- if (!fs.existsSync(hookMdPath)) {
144
- fs.writeFileSync(hookMdPath, HOOK_BLOCK, "utf8");
145
- process.stdout.write(`\u2713 Created ${hookMdPath}
146
-
147
- Restart OpenClaw \u2014 FastGRC will evaluate every tool call.
148
- `);
149
- process.exit(0);
150
- }
151
- const existing = fs.readFileSync(hookMdPath, "utf8");
152
- if (existing.includes(handlerStr)) {
153
- process.stdout.write(`\u2713 FastGRC hook already up to date in ${hookMdPath}
154
- `);
155
- process.exit(0);
225
+ doInstallHook(arg || process.cwd());
226
+ process.exit(0);
227
+ }
228
+ if (cmd === "uninstall-hook") {
229
+ doUninstallHook(arg || process.cwd());
230
+ process.exit(0);
231
+ }
232
+ if (cmd === "setup") {
233
+ const rest = process.argv.slice(3);
234
+ let apiKeyArg;
235
+ let policyIdArg;
236
+ for (let i = 0; i < rest.length; i++) {
237
+ if (rest[i] === "--api-key" && rest[i + 1]) {
238
+ apiKeyArg = rest[++i];
239
+ } else if (rest[i] === "--policy-id" && rest[i + 1]) {
240
+ policyIdArg = rest[++i];
241
+ } else if (!apiKeyArg && !rest[i].startsWith("--")) {
242
+ apiKeyArg = rest[i];
243
+ } else if (!policyIdArg && !rest[i].startsWith("--")) {
244
+ policyIdArg = rest[i];
245
+ }
156
246
  }
157
- if (existing.includes("fastgrc-hook") || existing.includes("fastgrc-openclaw")) {
158
- const patched = existing.replace(
159
- /handler:\s*"[^"]*(?:fastgrc-hook|fastgrc-openclaw)[^"]*"/,
160
- `handler: "${handlerStr}"`
161
- );
162
- fs.writeFileSync(hookMdPath, patched, "utf8");
163
- process.stdout.write(`\u2713 Updated handler in ${hookMdPath} \u2014 now uses absolute path.
164
-
165
- Restart OpenClaw to activate.
166
- `);
167
- process.exit(0);
247
+ if (!apiKeyArg) {
248
+ process.stderr.write("Usage: fastgrc-hook setup --api-key <key> [--policy-id <id>]\n");
249
+ process.exit(1);
168
250
  }
169
- const fmEnd = existing.indexOf("\n---", 3);
170
- let updated;
171
- if (fmEnd !== -1) {
172
- const hasHooksKey = existing.lastIndexOf("hooks:", fmEnd) !== -1;
173
- const insertText = hasHooksKey ? HOOK_ENTRY : `hooks:
174
- ${HOOK_ENTRY}`;
175
- updated = existing.slice(0, fmEnd) + "\n" + insertText + existing.slice(fmEnd);
251
+ writeConfig({ ...readConfig(), apiKey: apiKeyArg });
252
+ process.stdout.write("\u2713 API key saved to ~/.fastgrc.json\n");
253
+ if (policyIdArg) {
254
+ writeConfig({ ...readConfig(), policyId: policyIdArg });
255
+ process.stdout.write("\u2713 Policy ID saved to ~/.fastgrc.json\n");
176
256
  } else {
177
- updated = HOOK_BLOCK + "\n" + existing;
257
+ process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
178
258
  }
179
- fs.writeFileSync(hookMdPath, updated, "utf8");
180
- process.stdout.write(`\u2713 Updated ${hookMdPath} \u2014 FastGRC hook added.
181
-
182
- Restart OpenClaw to activate.
183
- `);
259
+ doInstallHook(process.cwd());
260
+ process.stdout.write("\n\u2713 Config and HOOK.md done.\n");
261
+ process.stdout.write('Run "fastgrc-hook test" to verify the hook.\n');
184
262
  process.exit(0);
185
263
  }
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");
264
+ if (cmd === "uninstall") {
265
+ const targetDir = arg || process.cwd();
266
+ const cfg = readConfig();
267
+ delete cfg.apiKey;
268
+ delete cfg.policyId;
269
+ writeConfig(cfg);
270
+ process.stdout.write("\u2713 API key and policy ID removed from ~/.fastgrc.json\n");
271
+ doUninstallHook(targetDir);
272
+ process.stdout.write(
273
+ "\n\u2713 Config and HOOK.md cleaned up.\nComplete removal \u2014 run these two commands:\n sudo openclaw plugins remove fastgrc-openclaw\n npm uninstall -g fastgrc-openclaw\n"
274
+ );
189
275
  process.exit(0);
190
276
  }
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 {
207
- }
208
- }
209
- if (!toolName && !raw) {
210
- process.exit(0);
211
- }
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
- }
277
+ if (cmd === "test") {
278
+ const testCommand = arg || "rm -rf /production-db";
279
+ const payload = JSON.stringify({
280
+ tool_name: "Bash",
281
+ tool_input: { command: testCommand },
282
+ session_id: "fastgrc-hook-test"
283
+ });
284
+ const { handlerStr } = computeHandler();
285
+ process.stdout.write(`Testing handler: ${handlerStr}
286
+ `);
287
+ process.stdout.write(`Payload: ${payload}
288
+
289
+ `);
290
+ const apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
291
+ if (!apiKey) {
292
+ process.stderr.write("No API key configured. Run: fastgrc-hook set-key fgrc_k1_...\n");
293
+ process.exit(1);
219
294
  }
220
- const result = await evaluate({
221
- toolName: resolvedToolName,
222
- args,
223
- agentId,
295
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
296
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
297
+ const ctx = JSON.parse(payload);
298
+ evaluate({
299
+ toolName: ctx.tool_name,
300
+ args: ctx.tool_input,
301
+ agentId: ctx.session_id,
224
302
  apiKey,
225
- // narrowed at module level via early-exit guard above
226
303
  policyId,
227
304
  baseUrl
305
+ }).then((result) => {
306
+ if (!result) {
307
+ process.stdout.write("Evaluate returned null (fail-open) \u2014 network error or timeout.\n");
308
+ process.exit(0);
309
+ }
310
+ process.stdout.write(`Decision: ${result.decision}
311
+ `);
312
+ if (result.reasoning) process.stdout.write(`Reason: ${result.reasoning}
313
+ `);
314
+ if (result.policyContext?.matchedRule) process.stdout.write(`Matched rule: ${result.policyContext.matchedRule}
315
+ `);
316
+ process.exit(result.decision === "block" || result.decision === "require_approval" ? 2 : 0);
317
+ }).catch((err) => {
318
+ process.stderr.write(`Test failed: ${err}
319
+ `);
320
+ process.exit(1);
228
321
  });
229
- if (!result) {
322
+ } else if (cmd === "where" || cmd === "which-hook") {
323
+ const { handlerStr, binPath } = computeHandler();
324
+ process.stdout.write(`Binary: ${binPath}
325
+ `);
326
+ process.stdout.write(`Handler: ${handlerStr}
327
+ `);
328
+ process.exit(0);
329
+ }
330
+ if (cmd === "test" || cmd === "where" || cmd === "which-hook" || cmd === "setup" || cmd === "uninstall" || cmd === "install-hook" || cmd === "uninstall-hook") {
331
+ } else {
332
+ const apiKey = process.env.FASTGRC_API_KEY ?? readConfig().apiKey;
333
+ if (!apiKey) {
334
+ process.stderr.write("[fastgrc-hook] No API key configured \u2014 run: fastgrc-hook set-key fgrc_k1_...\n");
230
335
  process.exit(0);
231
336
  }
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.
337
+ const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
338
+ const policyId = process.env.FASTGRC_POLICY_ID ?? readConfig().policyId;
339
+ async function main() {
340
+ const chunks = [];
341
+ for await (const chunk of process.stdin) chunks.push(chunk);
342
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
343
+ const toolName = process.env.HOOK_TOOL_NAME ?? "";
344
+ const toolInputRaw = process.env.HOOK_TOOL_INPUT ?? "{}";
345
+ let args = {};
346
+ let agentId;
347
+ if (raw) {
348
+ try {
349
+ const ctx = JSON.parse(raw);
350
+ args = ctx.tool_input ?? ctx.input ?? ctx.args ?? {};
351
+ agentId = ctx.agent_id ?? ctx.agentId ?? ctx.session_id;
352
+ } catch {
353
+ }
354
+ }
355
+ if (!toolName && !raw) {
356
+ process.exit(0);
357
+ }
358
+ const resolvedToolName = toolName || (raw ? JSON.parse(raw)?.tool_name ?? "unknown" : "unknown");
359
+ if (!args || Object.keys(args).length === 0) {
360
+ try {
361
+ args = JSON.parse(toolInputRaw);
362
+ } catch {
363
+ args = {};
364
+ }
365
+ }
366
+ const result = await evaluate({
367
+ toolName: resolvedToolName,
368
+ args,
369
+ agentId,
370
+ apiKey,
371
+ policyId,
372
+ baseUrl
373
+ });
374
+ if (!result) {
375
+ process.exit(0);
376
+ }
377
+ const { decision, reasoning, policyContext, reasonTags } = result;
378
+ const matchedRule = policyContext?.matchedRule;
379
+ if (decision === "block") {
380
+ const msg = matchedRule ? `FastGRC blocked: [${matchedRule}] ${reasoning}` : `FastGRC blocked: ${reasoning}`;
381
+ process.stderr.write(msg + "\n");
382
+ process.exit(2);
383
+ }
384
+ if (decision === "require_approval") {
385
+ process.stderr.write(
386
+ `FastGRC requires approval before this action.
242
387
  ${reasoning}
243
388
  Review at: ${baseUrl}/dashboard/agent-policies
244
389
  `
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");
390
+ );
391
+ process.exit(2);
392
+ }
393
+ if (Array.isArray(reasonTags) && reasonTags.includes("override_block_active")) {
394
+ const msg = matchedRule ? `[FastGRC] Observability mode \u2014 would have blocked: [${matchedRule}] ${reasoning}` : `[FastGRC] Observability mode \u2014 would have blocked: ${reasoning}`;
395
+ process.stderr.write(msg + "\n");
396
+ }
397
+ process.exit(0);
251
398
  }
252
- process.exit(0);
253
- }
254
- main().catch((err) => {
255
- process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
399
+ main().catch((err) => {
400
+ process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
256
401
  `);
257
- process.exit(0);
258
- });
402
+ process.exit(0);
403
+ });
404
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastgrc-openclaw",
3
- "version": "1.0.20",
3
+ "version": "1.0.24",
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",