@denial-web/clawguard 0.1.4 → 0.1.5

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/README.md CHANGED
@@ -68,6 +68,12 @@ npx @denial-web/clawguard hermes install ./candidate-skill --to ~/.hermes/skills
68
68
 
69
69
  The approval JSONL payload is designed for a bot or daemon to forward to WhatsApp, Telegram, Slack, Discord, or another owner channel before any files are copied into a trusted skill folder.
70
70
 
71
+ If OpenClaw already has messaging configured, ClawGuard can hand the approval message to OpenClaw:
72
+
73
+ ```bash
74
+ npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
75
+ ```
76
+
71
77
  When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
72
78
 
73
79
  ```bash
@@ -39,6 +39,33 @@ trusted skill folder
39
39
 
40
40
  If `--approval-out` is set, non-allow decisions create a pending approval JSON payload instead of copying files. With `--approval-mode always`, even allow decisions pause for explicit owner approval. A messaging adapter can forward the `message` field to WhatsApp, Telegram, Slack, Discord, or another owner channel.
41
41
 
42
+ ### Approval Message Delivery
43
+
44
+ Option A uses OpenClaw's native messaging command after OpenClaw is already configured by the user:
45
+
46
+ ```bash
47
+ clawguard approvals send ./.clawguard/approvals.jsonl \
48
+ --via openclaw \
49
+ --channel telegram \
50
+ --target 123456789
51
+ ```
52
+
53
+ The adapter calls:
54
+
55
+ ```bash
56
+ openclaw message send --channel telegram --target 123456789 --message "<approval message>"
57
+ ```
58
+
59
+ This is the easiest path for OpenClaw users because ClawGuard does not need to own Telegram, WhatsApp, Slack, or Discord credentials.
60
+
61
+ Option B is planned as a ClawGuard-owned sender:
62
+
63
+ ```bash
64
+ clawguard approvals serve --telegram
65
+ ```
66
+
67
+ That path is better when the user wants the approval channel to stay independent from the agent runtime.
68
+
42
69
  ### Skill Folder Scan
43
70
 
44
71
  Command:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denial-web/clawguard",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Explainable security scanner for OpenClaw-style skills and MCP tool configs.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  import { promises as fs } from "node:fs";
4
4
  import { randomUUID } from "node:crypto";
5
+ import { execFile } from "node:child_process";
5
6
  import path from "node:path";
7
+ import { promisify } from "node:util";
6
8
  import { loadConfig, mergeConfig, parseSize } from "./config.js";
7
9
  import { policyShouldFail } from "./policy.js";
8
10
  import { createHtmlReport } from "./reporters/html.js";
@@ -10,6 +12,7 @@ import { createSarifReport } from "./reporters/sarif.js";
10
12
  import { scanTarget } from "./scanner.js";
11
13
 
12
14
  const args = process.argv.slice(2);
15
+ const execFileAsync = promisify(execFile);
13
16
  const failLevels = ["none", "low", "medium", "high", "critical"];
14
17
  const policyPresets = ["personal", "governed", "enterprise"];
15
18
  const policyFailDecisions = ["warn", "manual_review", "sandbox_required", "dual_approval", "block"];
@@ -29,13 +32,24 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
29
32
  const commandContext = parseCommand(args);
30
33
  const { command, framework, optionValues } = commandContext;
31
34
 
32
- if (!["scan", "scan-workspace", "gate", "install"].includes(command)) {
35
+ if (!["scan", "scan-workspace", "gate", "install", "approvals-send"].includes(command)) {
33
36
  console.error(`Unknown command: ${command}`);
34
37
  printHelp();
35
38
  process.exit(1);
36
39
  }
37
40
 
38
41
  try {
42
+ if (command === "approvals-send") {
43
+ const sendOptions = parseApprovalSendOptions(optionValues);
44
+ const result = await sendApproval(sendOptions);
45
+ if (sendOptions.json) {
46
+ console.log(JSON.stringify(result, null, 2));
47
+ } else {
48
+ printApprovalSendResult(result);
49
+ }
50
+ process.exit(0);
51
+ }
52
+
39
53
  const cliOptions = parseOptions(optionValues);
40
54
  cliOptions.framework = framework;
41
55
  const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
@@ -95,6 +109,7 @@ Usage:
95
109
  clawguard install <path> --to <dir> [--policy <preset>] [--dry-run]
96
110
  clawguard openclaw install <path> --to <dir> [--approval-out <path>]
97
111
  clawguard hermes install <path> --to <dir> [--approval-out <path>]
112
+ clawguard approvals send <approval.json|approvals.jsonl> --via openclaw --channel <name> --target <id>
98
113
  clawguard scan-workspace <path> [--json] [--policy <preset>]
99
114
  npm run scan -- <path>
100
115
 
@@ -119,6 +134,11 @@ Options:
119
134
  --approval-out <path> Write a pending approval JSON request before copying.
120
135
  Use .jsonl to append JSON lines for bot/daemon integrations.
121
136
  --approval-mode <mode> Approval mode: non-allow, always. Default: non-allow.
137
+ --via <adapter> Approval send adapter. Currently: openclaw.
138
+ --channel <name> Messaging channel for approval send, such as telegram.
139
+ --target <id> Messaging target/chat id for approval send.
140
+ --sender-bin <path> Sender binary. Default for --via openclaw: openclaw.
141
+ --sender-arg <value> Extra argument before the generated sender command. Repeatable.
122
142
 
123
143
  Gate exit codes:
124
144
  0 = allow
@@ -131,6 +151,7 @@ Examples:
131
151
  npx @denial-web/clawguard install ./skills/my-skill --to ./.agents/skills --policy governed
132
152
  npx @denial-web/clawguard openclaw install ./skills/my-skill --to ./.agents/skills --approval-out ./.clawguard/approvals.jsonl
133
153
  npx @denial-web/clawguard hermes install ./skills/my-skill --to ~/.hermes/skills --approval-out ./.clawguard/approvals.jsonl
154
+ npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
134
155
  npm run scan -- examples/risky-skill
135
156
  npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
136
157
  npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
@@ -214,6 +235,14 @@ function printHumanResult(result, options) {
214
235
  function parseCommand(values) {
215
236
  const rawCommand = values[0];
216
237
 
238
+ if (rawCommand === "approvals" && values[1] === "send") {
239
+ return {
240
+ command: "approvals-send",
241
+ framework: undefined,
242
+ optionValues: values.slice(2)
243
+ };
244
+ }
245
+
217
246
  if (["openclaw", "hermes"].includes(rawCommand)) {
218
247
  const nestedCommand = values[1];
219
248
 
@@ -247,6 +276,103 @@ function parseCommand(values) {
247
276
  };
248
277
  }
249
278
 
279
+ async function sendApproval(options) {
280
+ const approval = await readApprovalRequest(options.approvalPath, options.id);
281
+ const message = String(approval.message ?? "").trim();
282
+
283
+ if (!message) {
284
+ throw new Error("Approval request has no message field.");
285
+ }
286
+
287
+ if (options.via !== "openclaw") {
288
+ throw new Error("Only --via openclaw is supported right now.");
289
+ }
290
+
291
+ const senderBin = options.senderBin ?? "openclaw";
292
+ const commandArgs = [
293
+ ...options.senderArgs,
294
+ "message",
295
+ "send",
296
+ "--channel",
297
+ options.channel,
298
+ "--target",
299
+ options.target,
300
+ "--message",
301
+ message
302
+ ];
303
+ const result = {
304
+ approval: {
305
+ id: approval.id,
306
+ status: approval.status,
307
+ decision: approval.decision,
308
+ risk: approval.risk,
309
+ framework: approval.framework
310
+ },
311
+ via: options.via,
312
+ channel: options.channel,
313
+ target: options.target,
314
+ senderBin,
315
+ command: [senderBin, ...commandArgs],
316
+ dryRun: options.dryRun,
317
+ sent: false,
318
+ stdout: "",
319
+ stderr: ""
320
+ };
321
+
322
+ if (options.dryRun) {
323
+ return result;
324
+ }
325
+
326
+ const output = await execFileAsync(senderBin, commandArgs, {
327
+ maxBuffer: 1024 * 1024
328
+ });
329
+
330
+ result.sent = true;
331
+ result.stdout = output.stdout;
332
+ result.stderr = output.stderr;
333
+ return result;
334
+ }
335
+
336
+ function printApprovalSendResult(result) {
337
+ console.log(`ClawGuard approval send: ${result.approval.id}`);
338
+ console.log(`Via: ${result.via}`);
339
+ console.log(`Channel: ${result.channel}`);
340
+ console.log(`Target: ${result.target}`);
341
+ console.log(`Decision: ${formatDecision(result.approval.decision ?? "unknown")}`);
342
+ console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
343
+ console.log(`Sent: ${result.sent ? "yes" : "no"}`);
344
+
345
+ if (result.dryRun) {
346
+ console.log(`Command: ${result.command.map(shellQuote).join(" ")}`);
347
+ }
348
+ }
349
+
350
+ async function readApprovalRequest(approvalPath, id) {
351
+ const resolvedPath = path.resolve(approvalPath);
352
+ const content = await fs.readFile(resolvedPath, "utf8");
353
+ const approvals = resolvedPath.endsWith(".jsonl")
354
+ ? content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
355
+ : [JSON.parse(content)];
356
+
357
+ if (approvals.length === 0) {
358
+ throw new Error(`No approval requests found in ${resolvedPath}`);
359
+ }
360
+
361
+ const approval = id
362
+ ? approvals.find((candidate) => candidate.id === id)
363
+ : approvals.at(-1);
364
+
365
+ if (!approval) {
366
+ throw new Error(`Approval request not found: ${id}`);
367
+ }
368
+
369
+ if (approval.schemaVersion !== "clawguard.approval.v1") {
370
+ throw new Error("Unsupported approval request schema.");
371
+ }
372
+
373
+ return approval;
374
+ }
375
+
250
376
  function printGateResult(result, options) {
251
377
  const decision = result.policy.decision;
252
378
  console.log(`ClawGuard gate: ${result.target}`);
@@ -588,6 +714,15 @@ function formatDecision(decision) {
588
714
  return decision.replaceAll("_", " ").toUpperCase();
589
715
  }
590
716
 
717
+ function shellQuote(value) {
718
+ const text = String(value);
719
+ if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) {
720
+ return text;
721
+ }
722
+
723
+ return `'${text.replaceAll("'", "'\\''")}'`;
724
+ }
725
+
591
726
  function gateExitCode(decision) {
592
727
  if (decision === "allow") {
593
728
  return 0;
@@ -745,6 +880,97 @@ function parseOptions(values) {
745
880
  return options;
746
881
  }
747
882
 
883
+ function parseApprovalSendOptions(values) {
884
+ const options = {
885
+ approvalPath: undefined,
886
+ id: undefined,
887
+ via: "openclaw",
888
+ channel: undefined,
889
+ target: undefined,
890
+ senderBin: undefined,
891
+ senderArgs: [],
892
+ dryRun: false,
893
+ json: false
894
+ };
895
+ const paths = [];
896
+
897
+ for (let index = 0; index < values.length; index += 1) {
898
+ const value = values[index];
899
+
900
+ if (value === "--json") {
901
+ options.json = true;
902
+ continue;
903
+ }
904
+
905
+ if (value === "--dry-run") {
906
+ options.dryRun = true;
907
+ continue;
908
+ }
909
+
910
+ if (value === "--id") {
911
+ options.id = requireNextValue(values, index, "--id");
912
+ index += 1;
913
+ continue;
914
+ }
915
+
916
+ if (value === "--via") {
917
+ options.via = requireNextValue(values, index, "--via");
918
+ index += 1;
919
+ continue;
920
+ }
921
+
922
+ if (value === "--channel") {
923
+ options.channel = requireNextValue(values, index, "--channel");
924
+ index += 1;
925
+ continue;
926
+ }
927
+
928
+ if (value === "--target") {
929
+ options.target = requireNextValue(values, index, "--target");
930
+ index += 1;
931
+ continue;
932
+ }
933
+
934
+ if (value === "--sender-bin") {
935
+ options.senderBin = requireNextValue(values, index, "--sender-bin");
936
+ index += 1;
937
+ continue;
938
+ }
939
+
940
+ if (value === "--sender-arg") {
941
+ options.senderArgs.push(requireNextValue(values, index, "--sender-arg"));
942
+ index += 1;
943
+ continue;
944
+ }
945
+
946
+ if (value.startsWith("--")) {
947
+ throw new Error(`Unknown option: ${value}`);
948
+ }
949
+
950
+ paths.push(value);
951
+ }
952
+
953
+ options.approvalPath = paths[0];
954
+
955
+ if (!options.approvalPath) {
956
+ throw new Error("approvals send requires <approval.json|approvals.jsonl>.");
957
+ }
958
+
959
+ if (options.via !== "openclaw") {
960
+ throw new Error("Invalid --via value. Use: openclaw");
961
+ }
962
+
963
+ if (!options.channel) {
964
+ throw new Error("approvals send requires --channel <name>.");
965
+ }
966
+
967
+ if (!options.target) {
968
+ throw new Error("approvals send requires --target <id>.");
969
+ }
970
+
971
+ return options;
972
+ }
973
+
748
974
  async function writeReportFile(outputPath, content) {
749
975
  const resolvedPath = path.resolve(outputPath);
750
976
  await fs.mkdir(path.dirname(resolvedPath), { recursive: true });