fastgrc-openclaw 1.0.29 → 1.0.32
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 +132 -72
- package/dist/bin.mjs +132 -72
- package/dist/plugin.d.mts +2 -2
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.js +65 -55
- package/dist/plugin.mjs +64 -54
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -95,26 +95,9 @@ args: ${JSON.stringify(args)}`;
|
|
|
95
95
|
|
|
96
96
|
// src/plugin.ts
|
|
97
97
|
var net = __toESM(require("net"));
|
|
98
|
-
var crypto = __toESM(require("crypto"));
|
|
99
98
|
var fs2 = __toESM(require("fs"));
|
|
100
99
|
var os2 = __toESM(require("os"));
|
|
101
100
|
var path2 = __toESM(require("path"));
|
|
102
|
-
function sign(payload, token) {
|
|
103
|
-
const nonce = crypto.randomBytes(16).toString("hex");
|
|
104
|
-
const ts = Math.floor(Date.now() / 1e3).toString();
|
|
105
|
-
const body = JSON.stringify(payload);
|
|
106
|
-
const mac = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${ts}:${body}`).digest("hex");
|
|
107
|
-
return { ...payload, nonce, timestamp: ts, hmac: mac };
|
|
108
|
-
}
|
|
109
|
-
function verifyHmac(req, token) {
|
|
110
|
-
try {
|
|
111
|
-
const { nonce, timestamp, hmac, ...rest } = req;
|
|
112
|
-
const expected = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${timestamp}:${JSON.stringify(rest)}`).digest("hex");
|
|
113
|
-
return hmac === expected;
|
|
114
|
-
} catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
101
|
function readExecApprovalsConfig() {
|
|
119
102
|
const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
|
|
120
103
|
try {
|
|
@@ -123,53 +106,80 @@ function readExecApprovalsConfig() {
|
|
|
123
106
|
return null;
|
|
124
107
|
}
|
|
125
108
|
}
|
|
126
|
-
function
|
|
109
|
+
function startExecApprovalsServer(apiKey, policyId, baseUrl) {
|
|
127
110
|
const cfg = readExecApprovalsConfig();
|
|
128
|
-
if (!cfg?.socket?.path || !cfg?.socket?.token)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
111
|
+
if (!cfg?.socket?.path || !cfg?.socket?.token) {
|
|
112
|
+
console.warn("[fastgrc] exec-approvals: no socket config found \u2014 webchat execs will not be evaluated");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const { path: sockPath, token: configToken } = cfg.socket;
|
|
116
|
+
try {
|
|
117
|
+
fs2.unlinkSync(sockPath);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
const server = net.createServer((conn) => {
|
|
132
121
|
let buffer = "";
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
122
|
+
conn.on("data", (chunk) => {
|
|
123
|
+
buffer += chunk.toString("utf8");
|
|
124
|
+
const idx = buffer.indexOf("\n");
|
|
125
|
+
if (idx === -1) return;
|
|
126
|
+
const line = buffer.slice(0, idx).trim();
|
|
127
|
+
buffer = "";
|
|
128
|
+
if (!line) {
|
|
129
|
+
conn.end();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let msg;
|
|
139
133
|
try {
|
|
140
|
-
|
|
134
|
+
msg = JSON.parse(line);
|
|
141
135
|
} catch {
|
|
136
|
+
conn.end();
|
|
142
137
|
return;
|
|
143
138
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
console.warn("[fastgrc] exec-approvals: HMAC verification failed \u2014 ignoring request");
|
|
139
|
+
if (msg.type !== "request") {
|
|
140
|
+
conn.end();
|
|
147
141
|
return;
|
|
148
142
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
143
|
+
if (msg.token !== configToken) {
|
|
144
|
+
console.warn("[fastgrc] exec-approvals: token mismatch \u2014 failing open. Restart fastgrc-approvals service if this persists.");
|
|
145
|
+
conn.end(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const req = msg.request ?? {};
|
|
149
|
+
const command = String(req.command ?? req.commandPreview ?? "unknown");
|
|
150
|
+
const agentId = req.agentId || void 0;
|
|
151
|
+
evaluate({
|
|
152
|
+
toolName: "Exec",
|
|
153
|
+
args: { command, cwd: req.cwd },
|
|
154
|
+
agentId,
|
|
155
|
+
apiKey,
|
|
156
|
+
policyId,
|
|
157
|
+
baseUrl
|
|
158
|
+
}).then((result) => {
|
|
159
|
+
let decision = "allow-once";
|
|
159
160
|
if (result && (result.decision === "block" || result.decision === "require_approval")) {
|
|
160
161
|
decision = "deny";
|
|
161
|
-
const
|
|
162
|
-
console.warn(
|
|
162
|
+
const rule = result.policyContext?.matchedRule;
|
|
163
|
+
console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
|
|
163
164
|
}
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
conn.end(JSON.stringify({ type: "decision", decision }) + "\n");
|
|
166
|
+
}).catch(() => {
|
|
167
|
+
conn.end(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
|
|
168
|
+
});
|
|
168
169
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
170
|
+
conn.on("error", () => {
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
server.listen(sockPath, () => {
|
|
174
|
+
try {
|
|
175
|
+
fs2.chmodSync(sockPath, 384);
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
console.log(`[fastgrc] exec-approvals server listening on ${sockPath}`);
|
|
179
|
+
});
|
|
180
|
+
server.on("error", (err) => {
|
|
181
|
+
console.warn(`[fastgrc] exec-approvals server error: ${err.message}`);
|
|
182
|
+
});
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
// src/bin.ts
|
|
@@ -279,6 +289,24 @@ function doUninstallHook(targetDir) {
|
|
|
279
289
|
`);
|
|
280
290
|
}
|
|
281
291
|
}
|
|
292
|
+
async function validatePolicy(apiKey, policyId, baseUrl = "https://app.fastgrc.ai") {
|
|
293
|
+
try {
|
|
294
|
+
const controller = new AbortController();
|
|
295
|
+
setTimeout(() => controller.abort(), 5e3);
|
|
296
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/policy-router/policies/${encodeURIComponent(policyId)}`, {
|
|
297
|
+
headers: { "Authorization": `Bearer ${apiKey}` },
|
|
298
|
+
signal: controller.signal
|
|
299
|
+
});
|
|
300
|
+
if (res.status === 404) return { valid: false, error: "Policy not found \u2014 check the ID belongs to your account." };
|
|
301
|
+
if (res.status === 401) return { valid: false, error: "Invalid API key." };
|
|
302
|
+
if (!res.ok) return { valid: false, error: `API returned ${res.status}` };
|
|
303
|
+
const policy = await res.json();
|
|
304
|
+
return { valid: true, name: policy.name };
|
|
305
|
+
} catch (err) {
|
|
306
|
+
const reason = err instanceof Error && err.name === "AbortError" ? "timeout" : String(err);
|
|
307
|
+
return { valid: false, error: `Could not reach FastGRC API (${reason}) \u2014 skipping validation.` };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
282
310
|
function doConfigureExecApprovals() {
|
|
283
311
|
const cfgPath = path3.join(os3.homedir(), ".openclaw", "exec-approvals.json");
|
|
284
312
|
if (!fs3.existsSync(cfgPath)) {
|
|
@@ -294,9 +322,9 @@ function doConfigureExecApprovals() {
|
|
|
294
322
|
const updated = {
|
|
295
323
|
...existing,
|
|
296
324
|
defaults: {
|
|
297
|
-
security: "
|
|
325
|
+
security: "ask",
|
|
298
326
|
ask: "always",
|
|
299
|
-
askFallback: "
|
|
327
|
+
askFallback: "allow",
|
|
300
328
|
autoAllowSkills: false
|
|
301
329
|
}
|
|
302
330
|
};
|
|
@@ -338,11 +366,29 @@ if (cmd === "set-policy") {
|
|
|
338
366
|
process.stderr.write("Usage: fastgrc-hook set-policy <policy-id>\n");
|
|
339
367
|
process.exit(1);
|
|
340
368
|
}
|
|
341
|
-
|
|
342
|
-
|
|
369
|
+
(async () => {
|
|
370
|
+
const apiKey = readConfig().apiKey ?? process.env.FASTGRC_API_KEY;
|
|
371
|
+
const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
|
|
372
|
+
if (apiKey) {
|
|
373
|
+
process.stdout.write(`Verifying policy ${arg}\u2026
|
|
343
374
|
`);
|
|
344
|
-
|
|
345
|
-
|
|
375
|
+
const check = await validatePolicy(apiKey, arg, baseUrl);
|
|
376
|
+
if (!check.valid) {
|
|
377
|
+
process.stderr.write(`\u2717 ${check.error}
|
|
378
|
+
`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
process.stdout.write(`\u2713 Policy verified: ${check.name}
|
|
382
|
+
`);
|
|
383
|
+
} else {
|
|
384
|
+
process.stdout.write("\u26A0 No API key set \u2014 skipping policy verification.\n");
|
|
385
|
+
}
|
|
386
|
+
writeConfig({ ...readConfig(), policyId: arg });
|
|
387
|
+
process.stdout.write(`\u2713 FastGRC policy ID saved to ${CONFIG_PATH}
|
|
388
|
+
`);
|
|
389
|
+
process.stdout.write(' Run "fastgrc-hook get-policy" to verify.\n');
|
|
390
|
+
process.exit(0);
|
|
391
|
+
})();
|
|
346
392
|
}
|
|
347
393
|
if (cmd === "get-policy") {
|
|
348
394
|
const id = readConfig().policyId;
|
|
@@ -389,19 +435,33 @@ if (cmd === "setup") {
|
|
|
389
435
|
process.stderr.write("Usage: fastgrc-hook setup --api-key <key> [--policy-id <id>]\n");
|
|
390
436
|
process.exit(1);
|
|
391
437
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
438
|
+
(async () => {
|
|
439
|
+
const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
|
|
440
|
+
writeConfig({ ...readConfig(), apiKey: apiKeyArg });
|
|
441
|
+
process.stdout.write("\u2713 API key saved to ~/.fastgrc.json\n");
|
|
442
|
+
if (policyIdArg) {
|
|
443
|
+
process.stdout.write(`Verifying policy ${policyIdArg}\u2026
|
|
444
|
+
`);
|
|
445
|
+
const check = await validatePolicy(apiKeyArg, policyIdArg, baseUrl);
|
|
446
|
+
if (!check.valid) {
|
|
447
|
+
process.stderr.write(`\u2717 Policy not found: ${check.error}
|
|
448
|
+
`);
|
|
449
|
+
process.stderr.write(" Pass a valid policy ID from your dashboard, or omit --policy-id to use the org default.\n");
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
process.stdout.write(`\u2713 Policy verified: ${check.name}
|
|
453
|
+
`);
|
|
454
|
+
writeConfig({ ...readConfig(), policyId: policyIdArg });
|
|
455
|
+
process.stdout.write("\u2713 Policy ID saved to ~/.fastgrc.json\n");
|
|
456
|
+
} else {
|
|
457
|
+
process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
|
|
458
|
+
}
|
|
459
|
+
doInstallHook(process.cwd());
|
|
460
|
+
doConfigureExecApprovals();
|
|
461
|
+
process.stdout.write("\n\u2713 Config, HOOK.md, and exec-approvals done.\n");
|
|
462
|
+
process.stdout.write('Restart OpenClaw, then run "fastgrc-hook test" to verify.\n');
|
|
463
|
+
process.exit(0);
|
|
464
|
+
})();
|
|
405
465
|
}
|
|
406
466
|
if (cmd === "serve-approvals") {
|
|
407
467
|
const apiKey = resolveApiKey();
|
|
@@ -420,7 +480,7 @@ if (cmd === "serve-approvals") {
|
|
|
420
480
|
const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
|
|
421
481
|
process.stdout.write(`[fastgrc] serve-approvals running \u2014 listening on ${cfg.socket.path}
|
|
422
482
|
`);
|
|
423
|
-
|
|
483
|
+
startExecApprovalsServer(apiKey, policyId, baseUrl);
|
|
424
484
|
}
|
|
425
485
|
if (cmd === "uninstall") {
|
|
426
486
|
const targetDir = arg || process.cwd();
|
package/dist/bin.mjs
CHANGED
|
@@ -72,26 +72,9 @@ args: ${JSON.stringify(args)}`;
|
|
|
72
72
|
|
|
73
73
|
// src/plugin.ts
|
|
74
74
|
import * as net from "net";
|
|
75
|
-
import * as crypto from "crypto";
|
|
76
75
|
import * as fs2 from "fs";
|
|
77
76
|
import * as os2 from "os";
|
|
78
77
|
import * as path2 from "path";
|
|
79
|
-
function sign(payload, token) {
|
|
80
|
-
const nonce = crypto.randomBytes(16).toString("hex");
|
|
81
|
-
const ts = Math.floor(Date.now() / 1e3).toString();
|
|
82
|
-
const body = JSON.stringify(payload);
|
|
83
|
-
const mac = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${ts}:${body}`).digest("hex");
|
|
84
|
-
return { ...payload, nonce, timestamp: ts, hmac: mac };
|
|
85
|
-
}
|
|
86
|
-
function verifyHmac(req, token) {
|
|
87
|
-
try {
|
|
88
|
-
const { nonce, timestamp, hmac, ...rest } = req;
|
|
89
|
-
const expected = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${timestamp}:${JSON.stringify(rest)}`).digest("hex");
|
|
90
|
-
return hmac === expected;
|
|
91
|
-
} catch {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
78
|
function readExecApprovalsConfig() {
|
|
96
79
|
const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
|
|
97
80
|
try {
|
|
@@ -100,53 +83,80 @@ function readExecApprovalsConfig() {
|
|
|
100
83
|
return null;
|
|
101
84
|
}
|
|
102
85
|
}
|
|
103
|
-
function
|
|
86
|
+
function startExecApprovalsServer(apiKey, policyId, baseUrl) {
|
|
104
87
|
const cfg = readExecApprovalsConfig();
|
|
105
|
-
if (!cfg?.socket?.path || !cfg?.socket?.token)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
88
|
+
if (!cfg?.socket?.path || !cfg?.socket?.token) {
|
|
89
|
+
console.warn("[fastgrc] exec-approvals: no socket config found \u2014 webchat execs will not be evaluated");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const { path: sockPath, token: configToken } = cfg.socket;
|
|
93
|
+
try {
|
|
94
|
+
fs2.unlinkSync(sockPath);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
const server = net.createServer((conn) => {
|
|
109
98
|
let buffer = "";
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
99
|
+
conn.on("data", (chunk) => {
|
|
100
|
+
buffer += chunk.toString("utf8");
|
|
101
|
+
const idx = buffer.indexOf("\n");
|
|
102
|
+
if (idx === -1) return;
|
|
103
|
+
const line = buffer.slice(0, idx).trim();
|
|
104
|
+
buffer = "";
|
|
105
|
+
if (!line) {
|
|
106
|
+
conn.end();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
let msg;
|
|
116
110
|
try {
|
|
117
|
-
|
|
111
|
+
msg = JSON.parse(line);
|
|
118
112
|
} catch {
|
|
113
|
+
conn.end();
|
|
119
114
|
return;
|
|
120
115
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
console.warn("[fastgrc] exec-approvals: HMAC verification failed \u2014 ignoring request");
|
|
116
|
+
if (msg.type !== "request") {
|
|
117
|
+
conn.end();
|
|
124
118
|
return;
|
|
125
119
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
120
|
+
if (msg.token !== configToken) {
|
|
121
|
+
console.warn("[fastgrc] exec-approvals: token mismatch \u2014 failing open. Restart fastgrc-approvals service if this persists.");
|
|
122
|
+
conn.end(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const req = msg.request ?? {};
|
|
126
|
+
const command = String(req.command ?? req.commandPreview ?? "unknown");
|
|
127
|
+
const agentId = req.agentId || void 0;
|
|
128
|
+
evaluate({
|
|
129
|
+
toolName: "Exec",
|
|
130
|
+
args: { command, cwd: req.cwd },
|
|
131
|
+
agentId,
|
|
132
|
+
apiKey,
|
|
133
|
+
policyId,
|
|
134
|
+
baseUrl
|
|
135
|
+
}).then((result) => {
|
|
136
|
+
let decision = "allow-once";
|
|
136
137
|
if (result && (result.decision === "block" || result.decision === "require_approval")) {
|
|
137
138
|
decision = "deny";
|
|
138
|
-
const
|
|
139
|
-
console.warn(
|
|
139
|
+
const rule = result.policyContext?.matchedRule;
|
|
140
|
+
console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
|
|
140
141
|
}
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
conn.end(JSON.stringify({ type: "decision", decision }) + "\n");
|
|
143
|
+
}).catch(() => {
|
|
144
|
+
conn.end(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
|
|
145
|
+
});
|
|
145
146
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
147
|
+
conn.on("error", () => {
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
server.listen(sockPath, () => {
|
|
151
|
+
try {
|
|
152
|
+
fs2.chmodSync(sockPath, 384);
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
console.log(`[fastgrc] exec-approvals server listening on ${sockPath}`);
|
|
156
|
+
});
|
|
157
|
+
server.on("error", (err) => {
|
|
158
|
+
console.warn(`[fastgrc] exec-approvals server error: ${err.message}`);
|
|
159
|
+
});
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
// src/bin.ts
|
|
@@ -256,6 +266,24 @@ function doUninstallHook(targetDir) {
|
|
|
256
266
|
`);
|
|
257
267
|
}
|
|
258
268
|
}
|
|
269
|
+
async function validatePolicy(apiKey, policyId, baseUrl = "https://app.fastgrc.ai") {
|
|
270
|
+
try {
|
|
271
|
+
const controller = new AbortController();
|
|
272
|
+
setTimeout(() => controller.abort(), 5e3);
|
|
273
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/policy-router/policies/${encodeURIComponent(policyId)}`, {
|
|
274
|
+
headers: { "Authorization": `Bearer ${apiKey}` },
|
|
275
|
+
signal: controller.signal
|
|
276
|
+
});
|
|
277
|
+
if (res.status === 404) return { valid: false, error: "Policy not found \u2014 check the ID belongs to your account." };
|
|
278
|
+
if (res.status === 401) return { valid: false, error: "Invalid API key." };
|
|
279
|
+
if (!res.ok) return { valid: false, error: `API returned ${res.status}` };
|
|
280
|
+
const policy = await res.json();
|
|
281
|
+
return { valid: true, name: policy.name };
|
|
282
|
+
} catch (err) {
|
|
283
|
+
const reason = err instanceof Error && err.name === "AbortError" ? "timeout" : String(err);
|
|
284
|
+
return { valid: false, error: `Could not reach FastGRC API (${reason}) \u2014 skipping validation.` };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
259
287
|
function doConfigureExecApprovals() {
|
|
260
288
|
const cfgPath = path3.join(os3.homedir(), ".openclaw", "exec-approvals.json");
|
|
261
289
|
if (!fs3.existsSync(cfgPath)) {
|
|
@@ -271,9 +299,9 @@ function doConfigureExecApprovals() {
|
|
|
271
299
|
const updated = {
|
|
272
300
|
...existing,
|
|
273
301
|
defaults: {
|
|
274
|
-
security: "
|
|
302
|
+
security: "ask",
|
|
275
303
|
ask: "always",
|
|
276
|
-
askFallback: "
|
|
304
|
+
askFallback: "allow",
|
|
277
305
|
autoAllowSkills: false
|
|
278
306
|
}
|
|
279
307
|
};
|
|
@@ -315,11 +343,29 @@ if (cmd === "set-policy") {
|
|
|
315
343
|
process.stderr.write("Usage: fastgrc-hook set-policy <policy-id>\n");
|
|
316
344
|
process.exit(1);
|
|
317
345
|
}
|
|
318
|
-
|
|
319
|
-
|
|
346
|
+
(async () => {
|
|
347
|
+
const apiKey = readConfig().apiKey ?? process.env.FASTGRC_API_KEY;
|
|
348
|
+
const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
|
|
349
|
+
if (apiKey) {
|
|
350
|
+
process.stdout.write(`Verifying policy ${arg}\u2026
|
|
320
351
|
`);
|
|
321
|
-
|
|
322
|
-
|
|
352
|
+
const check = await validatePolicy(apiKey, arg, baseUrl);
|
|
353
|
+
if (!check.valid) {
|
|
354
|
+
process.stderr.write(`\u2717 ${check.error}
|
|
355
|
+
`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
process.stdout.write(`\u2713 Policy verified: ${check.name}
|
|
359
|
+
`);
|
|
360
|
+
} else {
|
|
361
|
+
process.stdout.write("\u26A0 No API key set \u2014 skipping policy verification.\n");
|
|
362
|
+
}
|
|
363
|
+
writeConfig({ ...readConfig(), policyId: arg });
|
|
364
|
+
process.stdout.write(`\u2713 FastGRC policy ID saved to ${CONFIG_PATH}
|
|
365
|
+
`);
|
|
366
|
+
process.stdout.write(' Run "fastgrc-hook get-policy" to verify.\n');
|
|
367
|
+
process.exit(0);
|
|
368
|
+
})();
|
|
323
369
|
}
|
|
324
370
|
if (cmd === "get-policy") {
|
|
325
371
|
const id = readConfig().policyId;
|
|
@@ -366,19 +412,33 @@ if (cmd === "setup") {
|
|
|
366
412
|
process.stderr.write("Usage: fastgrc-hook setup --api-key <key> [--policy-id <id>]\n");
|
|
367
413
|
process.exit(1);
|
|
368
414
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
415
|
+
(async () => {
|
|
416
|
+
const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
|
|
417
|
+
writeConfig({ ...readConfig(), apiKey: apiKeyArg });
|
|
418
|
+
process.stdout.write("\u2713 API key saved to ~/.fastgrc.json\n");
|
|
419
|
+
if (policyIdArg) {
|
|
420
|
+
process.stdout.write(`Verifying policy ${policyIdArg}\u2026
|
|
421
|
+
`);
|
|
422
|
+
const check = await validatePolicy(apiKeyArg, policyIdArg, baseUrl);
|
|
423
|
+
if (!check.valid) {
|
|
424
|
+
process.stderr.write(`\u2717 Policy not found: ${check.error}
|
|
425
|
+
`);
|
|
426
|
+
process.stderr.write(" Pass a valid policy ID from your dashboard, or omit --policy-id to use the org default.\n");
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
process.stdout.write(`\u2713 Policy verified: ${check.name}
|
|
430
|
+
`);
|
|
431
|
+
writeConfig({ ...readConfig(), policyId: policyIdArg });
|
|
432
|
+
process.stdout.write("\u2713 Policy ID saved to ~/.fastgrc.json\n");
|
|
433
|
+
} else {
|
|
434
|
+
process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
|
|
435
|
+
}
|
|
436
|
+
doInstallHook(process.cwd());
|
|
437
|
+
doConfigureExecApprovals();
|
|
438
|
+
process.stdout.write("\n\u2713 Config, HOOK.md, and exec-approvals done.\n");
|
|
439
|
+
process.stdout.write('Restart OpenClaw, then run "fastgrc-hook test" to verify.\n');
|
|
440
|
+
process.exit(0);
|
|
441
|
+
})();
|
|
382
442
|
}
|
|
383
443
|
if (cmd === "serve-approvals") {
|
|
384
444
|
const apiKey = resolveApiKey();
|
|
@@ -397,7 +457,7 @@ if (cmd === "serve-approvals") {
|
|
|
397
457
|
const baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
|
|
398
458
|
process.stdout.write(`[fastgrc] serve-approvals running \u2014 listening on ${cfg.socket.path}
|
|
399
459
|
`);
|
|
400
|
-
|
|
460
|
+
startExecApprovalsServer(apiKey, policyId, baseUrl);
|
|
401
461
|
}
|
|
402
462
|
if (cmd === "uninstall") {
|
|
403
463
|
const targetDir = arg || process.cwd();
|
package/dist/plugin.d.mts
CHANGED
|
@@ -5,7 +5,7 @@ interface ExecApprovalsConfig {
|
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
7
|
declare function readExecApprovalsConfig(): ExecApprovalsConfig | null;
|
|
8
|
-
declare function
|
|
8
|
+
declare function startExecApprovalsServer(apiKey: string, policyId?: string, baseUrl?: string): void;
|
|
9
9
|
declare const pluginEntry: {
|
|
10
10
|
id: string;
|
|
11
11
|
name: string;
|
|
@@ -13,4 +13,4 @@ declare const pluginEntry: {
|
|
|
13
13
|
register(api: any): void;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
export { pluginEntry as default, readExecApprovalsConfig,
|
|
16
|
+
export { pluginEntry as default, readExecApprovalsConfig, startExecApprovalsServer };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ interface ExecApprovalsConfig {
|
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
7
|
declare function readExecApprovalsConfig(): ExecApprovalsConfig | null;
|
|
8
|
-
declare function
|
|
8
|
+
declare function startExecApprovalsServer(apiKey: string, policyId?: string, baseUrl?: string): void;
|
|
9
9
|
declare const pluginEntry: {
|
|
10
10
|
id: string;
|
|
11
11
|
name: string;
|
|
@@ -13,4 +13,4 @@ declare const pluginEntry: {
|
|
|
13
13
|
register(api: any): void;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
export { pluginEntry as default, readExecApprovalsConfig,
|
|
16
|
+
export { pluginEntry as default, readExecApprovalsConfig, startExecApprovalsServer };
|
package/dist/plugin.js
CHANGED
|
@@ -32,11 +32,10 @@ var plugin_exports = {};
|
|
|
32
32
|
__export(plugin_exports, {
|
|
33
33
|
default: () => plugin_default,
|
|
34
34
|
readExecApprovalsConfig: () => readExecApprovalsConfig,
|
|
35
|
-
|
|
35
|
+
startExecApprovalsServer: () => startExecApprovalsServer
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(plugin_exports);
|
|
38
38
|
var net = __toESM(require("net"));
|
|
39
|
-
var crypto = __toESM(require("crypto"));
|
|
40
39
|
var fs2 = __toESM(require("fs"));
|
|
41
40
|
var os2 = __toESM(require("os"));
|
|
42
41
|
var path2 = __toESM(require("path"));
|
|
@@ -121,22 +120,6 @@ args: ${JSON.stringify(args)}`;
|
|
|
121
120
|
// src/plugin.ts
|
|
122
121
|
var DEFAULT_BASE_URL2 = "https://app.fastgrc.ai";
|
|
123
122
|
var DEFAULT_TIMEOUT_MS2 = 3e3;
|
|
124
|
-
function sign(payload, token) {
|
|
125
|
-
const nonce = crypto.randomBytes(16).toString("hex");
|
|
126
|
-
const ts = Math.floor(Date.now() / 1e3).toString();
|
|
127
|
-
const body = JSON.stringify(payload);
|
|
128
|
-
const mac = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${ts}:${body}`).digest("hex");
|
|
129
|
-
return { ...payload, nonce, timestamp: ts, hmac: mac };
|
|
130
|
-
}
|
|
131
|
-
function verifyHmac(req, token) {
|
|
132
|
-
try {
|
|
133
|
-
const { nonce, timestamp, hmac, ...rest } = req;
|
|
134
|
-
const expected = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${timestamp}:${JSON.stringify(rest)}`).digest("hex");
|
|
135
|
-
return hmac === expected;
|
|
136
|
-
} catch {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
123
|
function readExecApprovalsConfig() {
|
|
141
124
|
const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
|
|
142
125
|
try {
|
|
@@ -145,53 +128,80 @@ function readExecApprovalsConfig() {
|
|
|
145
128
|
return null;
|
|
146
129
|
}
|
|
147
130
|
}
|
|
148
|
-
function
|
|
131
|
+
function startExecApprovalsServer(apiKey, policyId, baseUrl) {
|
|
149
132
|
const cfg = readExecApprovalsConfig();
|
|
150
|
-
if (!cfg?.socket?.path || !cfg?.socket?.token)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
133
|
+
if (!cfg?.socket?.path || !cfg?.socket?.token) {
|
|
134
|
+
console.warn("[fastgrc] exec-approvals: no socket config found \u2014 webchat execs will not be evaluated");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const { path: sockPath, token: configToken } = cfg.socket;
|
|
138
|
+
try {
|
|
139
|
+
fs2.unlinkSync(sockPath);
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
const server = net.createServer((conn) => {
|
|
154
143
|
let buffer = "";
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
144
|
+
conn.on("data", (chunk) => {
|
|
145
|
+
buffer += chunk.toString("utf8");
|
|
146
|
+
const idx = buffer.indexOf("\n");
|
|
147
|
+
if (idx === -1) return;
|
|
148
|
+
const line = buffer.slice(0, idx).trim();
|
|
149
|
+
buffer = "";
|
|
150
|
+
if (!line) {
|
|
151
|
+
conn.end();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let msg;
|
|
161
155
|
try {
|
|
162
|
-
|
|
156
|
+
msg = JSON.parse(line);
|
|
163
157
|
} catch {
|
|
158
|
+
conn.end();
|
|
164
159
|
return;
|
|
165
160
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
console.warn("[fastgrc] exec-approvals: HMAC verification failed \u2014 ignoring request");
|
|
161
|
+
if (msg.type !== "request") {
|
|
162
|
+
conn.end();
|
|
169
163
|
return;
|
|
170
164
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
165
|
+
if (msg.token !== configToken) {
|
|
166
|
+
console.warn("[fastgrc] exec-approvals: token mismatch \u2014 failing open. Restart fastgrc-approvals service if this persists.");
|
|
167
|
+
conn.end(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const req = msg.request ?? {};
|
|
171
|
+
const command = String(req.command ?? req.commandPreview ?? "unknown");
|
|
172
|
+
const agentId = req.agentId || void 0;
|
|
173
|
+
evaluate({
|
|
174
|
+
toolName: "Exec",
|
|
175
|
+
args: { command, cwd: req.cwd },
|
|
176
|
+
agentId,
|
|
177
|
+
apiKey,
|
|
178
|
+
policyId,
|
|
179
|
+
baseUrl
|
|
180
|
+
}).then((result) => {
|
|
181
|
+
let decision = "allow-once";
|
|
181
182
|
if (result && (result.decision === "block" || result.decision === "require_approval")) {
|
|
182
183
|
decision = "deny";
|
|
183
|
-
const
|
|
184
|
-
console.warn(
|
|
184
|
+
const rule = result.policyContext?.matchedRule;
|
|
185
|
+
console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
|
|
185
186
|
}
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
conn.end(JSON.stringify({ type: "decision", decision }) + "\n");
|
|
188
|
+
}).catch(() => {
|
|
189
|
+
conn.end(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
|
|
190
|
+
});
|
|
190
191
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
192
|
+
conn.on("error", () => {
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
server.listen(sockPath, () => {
|
|
196
|
+
try {
|
|
197
|
+
fs2.chmodSync(sockPath, 384);
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
console.log(`[fastgrc] exec-approvals server listening on ${sockPath}`);
|
|
201
|
+
});
|
|
202
|
+
server.on("error", (err) => {
|
|
203
|
+
console.warn(`[fastgrc] exec-approvals server error: ${err.message}`);
|
|
204
|
+
});
|
|
195
205
|
}
|
|
196
206
|
var pluginEntry = {
|
|
197
207
|
id: "fastgrc",
|
|
@@ -237,13 +247,13 @@ var pluginEntry = {
|
|
|
237
247
|
}
|
|
238
248
|
return void 0;
|
|
239
249
|
});
|
|
240
|
-
|
|
250
|
+
startExecApprovalsServer(apiKey, policyId, DEFAULT_BASE_URL2);
|
|
241
251
|
}
|
|
242
252
|
};
|
|
243
253
|
var plugin_default = pluginEntry;
|
|
244
254
|
// Annotate the CommonJS export names for ESM import in node:
|
|
245
255
|
0 && (module.exports = {
|
|
246
256
|
readExecApprovalsConfig,
|
|
247
|
-
|
|
257
|
+
startExecApprovalsServer
|
|
248
258
|
});
|
|
249
259
|
module.exports = module.exports.default ?? module.exports;
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/plugin.ts
|
|
2
2
|
import * as net from "net";
|
|
3
|
-
import * as crypto from "crypto";
|
|
4
3
|
import * as fs2 from "fs";
|
|
5
4
|
import * as os2 from "os";
|
|
6
5
|
import * as path2 from "path";
|
|
@@ -85,22 +84,6 @@ args: ${JSON.stringify(args)}`;
|
|
|
85
84
|
// src/plugin.ts
|
|
86
85
|
var DEFAULT_BASE_URL2 = "https://app.fastgrc.ai";
|
|
87
86
|
var DEFAULT_TIMEOUT_MS2 = 3e3;
|
|
88
|
-
function sign(payload, token) {
|
|
89
|
-
const nonce = crypto.randomBytes(16).toString("hex");
|
|
90
|
-
const ts = Math.floor(Date.now() / 1e3).toString();
|
|
91
|
-
const body = JSON.stringify(payload);
|
|
92
|
-
const mac = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${ts}:${body}`).digest("hex");
|
|
93
|
-
return { ...payload, nonce, timestamp: ts, hmac: mac };
|
|
94
|
-
}
|
|
95
|
-
function verifyHmac(req, token) {
|
|
96
|
-
try {
|
|
97
|
-
const { nonce, timestamp, hmac, ...rest } = req;
|
|
98
|
-
const expected = crypto.createHmac("sha256", Buffer.from(token, "base64")).update(`${nonce}:${timestamp}:${JSON.stringify(rest)}`).digest("hex");
|
|
99
|
-
return hmac === expected;
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
87
|
function readExecApprovalsConfig() {
|
|
105
88
|
const cfgPath = path2.join(os2.homedir(), ".openclaw", "exec-approvals.json");
|
|
106
89
|
try {
|
|
@@ -109,53 +92,80 @@ function readExecApprovalsConfig() {
|
|
|
109
92
|
return null;
|
|
110
93
|
}
|
|
111
94
|
}
|
|
112
|
-
function
|
|
95
|
+
function startExecApprovalsServer(apiKey, policyId, baseUrl) {
|
|
113
96
|
const cfg = readExecApprovalsConfig();
|
|
114
|
-
if (!cfg?.socket?.path || !cfg?.socket?.token)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
97
|
+
if (!cfg?.socket?.path || !cfg?.socket?.token) {
|
|
98
|
+
console.warn("[fastgrc] exec-approvals: no socket config found \u2014 webchat execs will not be evaluated");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const { path: sockPath, token: configToken } = cfg.socket;
|
|
102
|
+
try {
|
|
103
|
+
fs2.unlinkSync(sockPath);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
const server = net.createServer((conn) => {
|
|
118
107
|
let buffer = "";
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
108
|
+
conn.on("data", (chunk) => {
|
|
109
|
+
buffer += chunk.toString("utf8");
|
|
110
|
+
const idx = buffer.indexOf("\n");
|
|
111
|
+
if (idx === -1) return;
|
|
112
|
+
const line = buffer.slice(0, idx).trim();
|
|
113
|
+
buffer = "";
|
|
114
|
+
if (!line) {
|
|
115
|
+
conn.end();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
let msg;
|
|
125
119
|
try {
|
|
126
|
-
|
|
120
|
+
msg = JSON.parse(line);
|
|
127
121
|
} catch {
|
|
122
|
+
conn.end();
|
|
128
123
|
return;
|
|
129
124
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.warn("[fastgrc] exec-approvals: HMAC verification failed \u2014 ignoring request");
|
|
125
|
+
if (msg.type !== "request") {
|
|
126
|
+
conn.end();
|
|
133
127
|
return;
|
|
134
128
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
129
|
+
if (msg.token !== configToken) {
|
|
130
|
+
console.warn("[fastgrc] exec-approvals: token mismatch \u2014 failing open. Restart fastgrc-approvals service if this persists.");
|
|
131
|
+
conn.end(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const req = msg.request ?? {};
|
|
135
|
+
const command = String(req.command ?? req.commandPreview ?? "unknown");
|
|
136
|
+
const agentId = req.agentId || void 0;
|
|
137
|
+
evaluate({
|
|
138
|
+
toolName: "Exec",
|
|
139
|
+
args: { command, cwd: req.cwd },
|
|
140
|
+
agentId,
|
|
141
|
+
apiKey,
|
|
142
|
+
policyId,
|
|
143
|
+
baseUrl
|
|
144
|
+
}).then((result) => {
|
|
145
|
+
let decision = "allow-once";
|
|
145
146
|
if (result && (result.decision === "block" || result.decision === "require_approval")) {
|
|
146
147
|
decision = "deny";
|
|
147
|
-
const
|
|
148
|
-
console.warn(
|
|
148
|
+
const rule = result.policyContext?.matchedRule;
|
|
149
|
+
console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
conn.end(JSON.stringify({ type: "decision", decision }) + "\n");
|
|
152
|
+
}).catch(() => {
|
|
153
|
+
conn.end(JSON.stringify({ type: "decision", decision: "allow-once" }) + "\n");
|
|
154
|
+
});
|
|
154
155
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
156
|
+
conn.on("error", () => {
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
server.listen(sockPath, () => {
|
|
160
|
+
try {
|
|
161
|
+
fs2.chmodSync(sockPath, 384);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
console.log(`[fastgrc] exec-approvals server listening on ${sockPath}`);
|
|
165
|
+
});
|
|
166
|
+
server.on("error", (err) => {
|
|
167
|
+
console.warn(`[fastgrc] exec-approvals server error: ${err.message}`);
|
|
168
|
+
});
|
|
159
169
|
}
|
|
160
170
|
var pluginEntry = {
|
|
161
171
|
id: "fastgrc",
|
|
@@ -201,12 +211,12 @@ var pluginEntry = {
|
|
|
201
211
|
}
|
|
202
212
|
return void 0;
|
|
203
213
|
});
|
|
204
|
-
|
|
214
|
+
startExecApprovalsServer(apiKey, policyId, DEFAULT_BASE_URL2);
|
|
205
215
|
}
|
|
206
216
|
};
|
|
207
217
|
var plugin_default = pluginEntry;
|
|
208
218
|
export {
|
|
209
219
|
plugin_default as default,
|
|
210
220
|
readExecApprovalsConfig,
|
|
211
|
-
|
|
221
|
+
startExecApprovalsServer
|
|
212
222
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastgrc-openclaw",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
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",
|