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 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 startExecApprovalsClient(apiKey, policyId, baseUrl) {
109
+ function startExecApprovalsServer(apiKey, policyId, baseUrl) {
127
110
  const cfg = readExecApprovalsConfig();
128
- if (!cfg?.socket?.path || !cfg?.socket?.token) return;
129
- const { path: sockPath, token } = cfg.socket;
130
- function connect() {
131
- const client = net.createConnection(sockPath);
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
- client.on("connect", () => {
134
- console.log("[fastgrc] exec-approvals socket connected");
135
- });
136
- client.on("data", async (buf) => {
137
- buffer += buf.toString();
138
- let req;
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
- req = JSON.parse(buffer);
134
+ msg = JSON.parse(line);
141
135
  } catch {
136
+ conn.end();
142
137
  return;
143
138
  }
144
- buffer = "";
145
- if (!verifyHmac(req, token)) {
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
- let decision = "allow-once";
150
- try {
151
- const result = await evaluate({
152
- toolName: "Exec",
153
- args: { command: req.command, rawCommand: req.rawCommand, cwd: req.cwd },
154
- agentId: req.agentId,
155
- apiKey,
156
- policyId,
157
- baseUrl
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 msg = result.policyContext?.matchedRule ? `[fastgrc] exec blocked: [${result.policyContext.matchedRule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`;
162
- console.warn(msg);
162
+ const rule = result.policyContext?.matchedRule;
163
+ console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
163
164
  }
164
- } catch {
165
- }
166
- const response = sign({ approvalId: req.approvalId, decision }, token);
167
- client.write(JSON.stringify(response));
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
- client.on("error", () => setTimeout(connect, 5e3));
170
- client.on("close", () => setTimeout(connect, 5e3));
171
- }
172
- connect();
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: "deny",
325
+ security: "ask",
298
326
  ask: "always",
299
- askFallback: "deny",
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
- writeConfig({ ...readConfig(), policyId: arg });
342
- process.stdout.write(`\u2713 FastGRC policy ID saved to ${CONFIG_PATH}
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
- process.stdout.write(' Run "fastgrc-hook get-policy" to verify.\n');
345
- process.exit(0);
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
- writeConfig({ ...readConfig(), apiKey: apiKeyArg });
393
- process.stdout.write("\u2713 API key saved to ~/.fastgrc.json\n");
394
- if (policyIdArg) {
395
- writeConfig({ ...readConfig(), policyId: policyIdArg });
396
- process.stdout.write("\u2713 Policy ID saved to ~/.fastgrc.json\n");
397
- } else {
398
- process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
399
- }
400
- doInstallHook(process.cwd());
401
- doConfigureExecApprovals();
402
- process.stdout.write("\n\u2713 Config, HOOK.md, and exec-approvals done.\n");
403
- process.stdout.write('Restart OpenClaw, then run "fastgrc-hook test" to verify.\n');
404
- process.exit(0);
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
- startExecApprovalsClient(apiKey, policyId, baseUrl);
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 startExecApprovalsClient(apiKey, policyId, baseUrl) {
86
+ function startExecApprovalsServer(apiKey, policyId, baseUrl) {
104
87
  const cfg = readExecApprovalsConfig();
105
- if (!cfg?.socket?.path || !cfg?.socket?.token) return;
106
- const { path: sockPath, token } = cfg.socket;
107
- function connect() {
108
- const client = net.createConnection(sockPath);
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
- client.on("connect", () => {
111
- console.log("[fastgrc] exec-approvals socket connected");
112
- });
113
- client.on("data", async (buf) => {
114
- buffer += buf.toString();
115
- let req;
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
- req = JSON.parse(buffer);
111
+ msg = JSON.parse(line);
118
112
  } catch {
113
+ conn.end();
119
114
  return;
120
115
  }
121
- buffer = "";
122
- if (!verifyHmac(req, token)) {
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
- let decision = "allow-once";
127
- try {
128
- const result = await evaluate({
129
- toolName: "Exec",
130
- args: { command: req.command, rawCommand: req.rawCommand, cwd: req.cwd },
131
- agentId: req.agentId,
132
- apiKey,
133
- policyId,
134
- baseUrl
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 msg = result.policyContext?.matchedRule ? `[fastgrc] exec blocked: [${result.policyContext.matchedRule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`;
139
- console.warn(msg);
139
+ const rule = result.policyContext?.matchedRule;
140
+ console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
140
141
  }
141
- } catch {
142
- }
143
- const response = sign({ approvalId: req.approvalId, decision }, token);
144
- client.write(JSON.stringify(response));
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
- client.on("error", () => setTimeout(connect, 5e3));
147
- client.on("close", () => setTimeout(connect, 5e3));
148
- }
149
- connect();
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: "deny",
302
+ security: "ask",
275
303
  ask: "always",
276
- askFallback: "deny",
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
- writeConfig({ ...readConfig(), policyId: arg });
319
- process.stdout.write(`\u2713 FastGRC policy ID saved to ${CONFIG_PATH}
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
- process.stdout.write(' Run "fastgrc-hook get-policy" to verify.\n');
322
- process.exit(0);
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
- writeConfig({ ...readConfig(), apiKey: apiKeyArg });
370
- process.stdout.write("\u2713 API key saved to ~/.fastgrc.json\n");
371
- if (policyIdArg) {
372
- writeConfig({ ...readConfig(), policyId: policyIdArg });
373
- process.stdout.write("\u2713 Policy ID saved to ~/.fastgrc.json\n");
374
- } else {
375
- process.stdout.write(" (no policy ID \u2014 org-wide default will be used)\n");
376
- }
377
- doInstallHook(process.cwd());
378
- doConfigureExecApprovals();
379
- process.stdout.write("\n\u2713 Config, HOOK.md, and exec-approvals done.\n");
380
- process.stdout.write('Restart OpenClaw, then run "fastgrc-hook test" to verify.\n');
381
- process.exit(0);
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
- startExecApprovalsClient(apiKey, policyId, baseUrl);
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 startExecApprovalsClient(apiKey: string, policyId?: string, baseUrl?: string): void;
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, startExecApprovalsClient };
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 startExecApprovalsClient(apiKey: string, policyId?: string, baseUrl?: string): void;
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, startExecApprovalsClient };
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
- startExecApprovalsClient: () => startExecApprovalsClient
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 startExecApprovalsClient(apiKey, policyId, baseUrl) {
131
+ function startExecApprovalsServer(apiKey, policyId, baseUrl) {
149
132
  const cfg = readExecApprovalsConfig();
150
- if (!cfg?.socket?.path || !cfg?.socket?.token) return;
151
- const { path: sockPath, token } = cfg.socket;
152
- function connect() {
153
- const client = net.createConnection(sockPath);
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
- client.on("connect", () => {
156
- console.log("[fastgrc] exec-approvals socket connected");
157
- });
158
- client.on("data", async (buf) => {
159
- buffer += buf.toString();
160
- let req;
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
- req = JSON.parse(buffer);
156
+ msg = JSON.parse(line);
163
157
  } catch {
158
+ conn.end();
164
159
  return;
165
160
  }
166
- buffer = "";
167
- if (!verifyHmac(req, token)) {
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
- let decision = "allow-once";
172
- try {
173
- const result = await evaluate({
174
- toolName: "Exec",
175
- args: { command: req.command, rawCommand: req.rawCommand, cwd: req.cwd },
176
- agentId: req.agentId,
177
- apiKey,
178
- policyId,
179
- baseUrl
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 msg = result.policyContext?.matchedRule ? `[fastgrc] exec blocked: [${result.policyContext.matchedRule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`;
184
- console.warn(msg);
184
+ const rule = result.policyContext?.matchedRule;
185
+ console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
185
186
  }
186
- } catch {
187
- }
188
- const response = sign({ approvalId: req.approvalId, decision }, token);
189
- client.write(JSON.stringify(response));
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
- client.on("error", () => setTimeout(connect, 5e3));
192
- client.on("close", () => setTimeout(connect, 5e3));
193
- }
194
- connect();
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
- startExecApprovalsClient(apiKey, policyId, DEFAULT_BASE_URL2);
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
- startExecApprovalsClient
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 startExecApprovalsClient(apiKey, policyId, baseUrl) {
95
+ function startExecApprovalsServer(apiKey, policyId, baseUrl) {
113
96
  const cfg = readExecApprovalsConfig();
114
- if (!cfg?.socket?.path || !cfg?.socket?.token) return;
115
- const { path: sockPath, token } = cfg.socket;
116
- function connect() {
117
- const client = net.createConnection(sockPath);
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
- client.on("connect", () => {
120
- console.log("[fastgrc] exec-approvals socket connected");
121
- });
122
- client.on("data", async (buf) => {
123
- buffer += buf.toString();
124
- let req;
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
- req = JSON.parse(buffer);
120
+ msg = JSON.parse(line);
127
121
  } catch {
122
+ conn.end();
128
123
  return;
129
124
  }
130
- buffer = "";
131
- if (!verifyHmac(req, token)) {
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
- let decision = "allow-once";
136
- try {
137
- const result = await evaluate({
138
- toolName: "Exec",
139
- args: { command: req.command, rawCommand: req.rawCommand, cwd: req.cwd },
140
- agentId: req.agentId,
141
- apiKey,
142
- policyId,
143
- baseUrl
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 msg = result.policyContext?.matchedRule ? `[fastgrc] exec blocked: [${result.policyContext.matchedRule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`;
148
- console.warn(msg);
148
+ const rule = result.policyContext?.matchedRule;
149
+ console.warn(rule ? `[fastgrc] exec blocked: [${rule}] ${result.reasoning}` : `[fastgrc] exec blocked: ${result.reasoning}`);
149
150
  }
150
- } catch {
151
- }
152
- const response = sign({ approvalId: req.approvalId, decision }, token);
153
- client.write(JSON.stringify(response));
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
- client.on("error", () => setTimeout(connect, 5e3));
156
- client.on("close", () => setTimeout(connect, 5e3));
157
- }
158
- connect();
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
- startExecApprovalsClient(apiKey, policyId, DEFAULT_BASE_URL2);
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
- startExecApprovalsClient
221
+ startExecApprovalsServer
212
222
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastgrc-openclaw",
3
- "version": "1.0.29",
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",