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.
- package/dist/bin.js +261 -115
- package/dist/bin.mjs +261 -115
- 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
|
|
42
|
-
policyId
|
|
43
|
-
baseUrl
|
|
41
|
+
apiKey,
|
|
42
|
+
policyId,
|
|
43
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
44
44
|
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
45
45
|
} = payload;
|
|
46
|
-
const evalUrl = `${
|
|
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 ${
|
|
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
|
-
...
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 (
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
280
|
+
process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
|
|
201
281
|
}
|
|
202
|
-
|
|
203
|
-
process.stdout.write(
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
process.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
19
|
-
policyId
|
|
20
|
-
baseUrl
|
|
18
|
+
apiKey,
|
|
19
|
+
policyId,
|
|
20
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
21
21
|
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
22
22
|
} = payload;
|
|
23
|
-
const evalUrl = `${
|
|
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 ${
|
|
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
|
-
...
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 (
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
257
|
+
process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
|
|
178
258
|
}
|
|
179
|
-
|
|
180
|
-
process.stdout.write(
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
process.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|