@denial-web/clawguard 0.1.8 → 0.1.9

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
@@ -92,6 +92,13 @@ Use `--once --dry-run` to verify the message flow without sending anything:
92
92
  TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789 --once --dry-run
93
93
  ```
94
94
 
95
+ Record the owner's decision in a durable decision log:
96
+
97
+ ```bash
98
+ npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <approval-id> --decision approve --out ./.clawguard/decisions.jsonl
99
+ npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <approval-id> --decision deny --reason "Unexpected shell access" --out ./.clawguard/decisions.jsonl
100
+ ```
101
+
95
102
  When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
96
103
 
97
104
  ```bash
@@ -78,6 +78,21 @@ TELEGRAM_BOT_TOKEN=123456:token clawguard approvals watch ./.clawguard/approvals
78
78
 
79
79
  The watcher keeps search and discovery unrestricted. It only reacts after a guarded install writes a pending approval request. By default it records sent ids in `./.clawguard/approvals.jsonl.sent.json`, so restarting the bridge does not resend the same request. Use `--once --dry-run` for setup checks and CI smoke tests.
80
80
 
81
+ ### Approval Decisions
82
+
83
+ Owner decisions are stored separately from approval requests. This keeps the original scan evidence immutable and gives messaging bridges a simple append-only target:
84
+
85
+ ```bash
86
+ clawguard approvals decide ./.clawguard/approvals.jsonl \
87
+ --id <approval-id> \
88
+ --decision approve \
89
+ --actor owner \
90
+ --reason "Reviewed and acceptable" \
91
+ --out ./.clawguard/decisions.jsonl
92
+ ```
93
+
94
+ The decision log uses `schemaVersion: "clawguard.decision.v1"` and stores the approval id, normalized decision, actor, reason, target, destination, risk summary, policy summary, and source approval path. Later Telegram, WhatsApp, OpenClaw, or Hermes bridges should write this same decision format after parsing owner replies.
95
+
81
96
  ### Skill Folder Scan
82
97
 
83
98
  Command:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denial-web/clawguard",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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
@@ -32,7 +32,15 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
32
32
  const commandContext = parseCommand(args);
33
33
  const { command, framework, optionValues } = commandContext;
34
34
 
35
- if (!["scan", "scan-workspace", "gate", "install", "approvals-send", "approvals-watch"].includes(command)) {
35
+ if (![
36
+ "scan",
37
+ "scan-workspace",
38
+ "gate",
39
+ "install",
40
+ "approvals-send",
41
+ "approvals-watch",
42
+ "approvals-decide"
43
+ ].includes(command)) {
36
44
  console.error(`Unknown command: ${command}`);
37
45
  printHelp();
38
46
  process.exit(1);
@@ -63,6 +71,17 @@ try {
63
71
  process.exit(0);
64
72
  }
65
73
 
74
+ if (command === "approvals-decide") {
75
+ const decisionOptions = parseApprovalDecisionOptions(optionValues);
76
+ const result = await decideApproval(decisionOptions);
77
+ if (decisionOptions.json) {
78
+ console.log(JSON.stringify(result, null, 2));
79
+ } else {
80
+ printApprovalDecisionResult(result);
81
+ }
82
+ process.exit(0);
83
+ }
84
+
66
85
  const cliOptions = parseOptions(optionValues);
67
86
  cliOptions.framework = framework;
68
87
  const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
@@ -125,6 +144,7 @@ Usage:
125
144
  clawguard approvals send <approval.json|approvals.jsonl> --via openclaw --channel <name> --target <id>
126
145
  clawguard approvals send <approval.json|approvals.jsonl> --via telegram --chat-id <id>
127
146
  clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
147
+ clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
128
148
  clawguard scan-workspace <path> [--json] [--policy <preset>]
129
149
  npm run scan -- <path>
130
150
 
@@ -159,6 +179,11 @@ Options:
159
179
  --interval <ms> Approval watch poll interval. Default: 2000.
160
180
  --state <path> Approval watch sent-id state file.
161
181
  --once Run approval watch once and exit.
182
+ --id <id> Approval id for send or decide.
183
+ --decision <value> Approval decision: approve, deny.
184
+ --out <path> Decision JSONL output file.
185
+ --actor <name> Decision actor. Default: local-user.
186
+ --reason <text> Decision reason.
162
187
 
163
188
  Gate exit codes:
164
189
  0 = allow
@@ -174,6 +199,7 @@ Examples:
174
199
  npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
175
200
  npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
176
201
  npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
202
+ npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
177
203
  npm run scan -- examples/risky-skill
178
204
  npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
179
205
  npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
@@ -273,6 +299,14 @@ function parseCommand(values) {
273
299
  };
274
300
  }
275
301
 
302
+ if (rawCommand === "approvals" && values[1] === "decide") {
303
+ return {
304
+ command: "approvals-decide",
305
+ framework: undefined,
306
+ optionValues: values.slice(2)
307
+ };
308
+ }
309
+
276
310
  if (["openclaw", "hermes"].includes(rawCommand)) {
277
311
  const nestedCommand = values[1];
278
312
 
@@ -432,6 +466,52 @@ async function watchApprovals(options, hooks = {}) {
432
466
  } while (true);
433
467
  }
434
468
 
469
+ async function decideApproval(options) {
470
+ const approval = await readApprovalRequest(options.approvalPath, options.id);
471
+ const outputPath = path.resolve(options.outPath ?? `${options.approvalPath}.decisions.jsonl`);
472
+ const decision = createApprovalDecision(approval, options);
473
+
474
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
475
+ await fs.appendFile(outputPath, `${JSON.stringify(decision)}\n`);
476
+
477
+ return {
478
+ approval: {
479
+ id: approval.id,
480
+ status: approval.status,
481
+ decision: approval.decision,
482
+ risk: approval.risk,
483
+ framework: approval.framework
484
+ },
485
+ outputPath,
486
+ decision
487
+ };
488
+ }
489
+
490
+ function createApprovalDecision(approval, options) {
491
+ const decision = normalizeApprovalDecision(options.decision);
492
+ const status = decision === "approve" ? "approved" : "denied";
493
+
494
+ return {
495
+ schemaVersion: "clawguard.decision.v1",
496
+ id: randomUUID(),
497
+ approvalId: approval.id,
498
+ status,
499
+ decision,
500
+ decidedAt: new Date().toISOString(),
501
+ actor: options.actor,
502
+ reason: options.reason,
503
+ framework: approval.framework,
504
+ target: approval.target,
505
+ destination: approval.destination,
506
+ risk: approval.risk,
507
+ policy: approval.policy,
508
+ source: {
509
+ path: path.resolve(options.approvalPath),
510
+ approvalCreatedAt: approval.createdAt
511
+ }
512
+ };
513
+ }
514
+
435
515
  async function sendTelegramApproval(approval, message, options) {
436
516
  const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
437
517
 
@@ -533,6 +613,17 @@ function printApprovalWatchResult(result) {
533
613
  console.log(`Skipped: ${result.skipped}`);
534
614
  }
535
615
 
616
+ function printApprovalDecisionResult(result) {
617
+ console.log(`ClawGuard approval decision: ${result.approval.id}`);
618
+ console.log(`Decision: ${formatDecision(result.decision.decision)}`);
619
+ console.log(`Status: ${result.decision.status}`);
620
+ console.log(`Actor: ${result.decision.actor}`);
621
+ if (result.decision.reason) {
622
+ console.log(`Reason: ${result.decision.reason}`);
623
+ }
624
+ console.log(`Output: ${result.outputPath}`);
625
+ }
626
+
536
627
  async function readApprovalRequest(approvalPath, id) {
537
628
  const resolvedPath = path.resolve(approvalPath);
538
629
  const approvals = await readApprovalRequests(resolvedPath);
@@ -931,6 +1022,10 @@ function commandLabel(commandName) {
931
1022
  return "Approval watch";
932
1023
  }
933
1024
 
1025
+ if (commandName === "approvals-decide") {
1026
+ return "Approval decision";
1027
+ }
1028
+
934
1029
  if (commandName === "gate") {
935
1030
  return "Gate";
936
1031
  }
@@ -977,6 +1072,18 @@ function sleep(ms) {
977
1072
  });
978
1073
  }
979
1074
 
1075
+ function normalizeApprovalDecision(value) {
1076
+ if (value === "approved") {
1077
+ return "approve";
1078
+ }
1079
+
1080
+ if (value === "denied") {
1081
+ return "deny";
1082
+ }
1083
+
1084
+ return value;
1085
+ }
1086
+
980
1087
  function gateExitCode(decision) {
981
1088
  if (decision === "allow") {
982
1089
  return 0;
@@ -1393,6 +1500,86 @@ function parseApprovalWatchOptions(values) {
1393
1500
  return options;
1394
1501
  }
1395
1502
 
1503
+ function parseApprovalDecisionOptions(values) {
1504
+ const options = {
1505
+ approvalPath: undefined,
1506
+ id: undefined,
1507
+ decision: undefined,
1508
+ outPath: undefined,
1509
+ actor: "local-user",
1510
+ reason: undefined,
1511
+ json: false
1512
+ };
1513
+ const paths = [];
1514
+
1515
+ for (let index = 0; index < values.length; index += 1) {
1516
+ const value = values[index];
1517
+
1518
+ if (value === "--json") {
1519
+ options.json = true;
1520
+ continue;
1521
+ }
1522
+
1523
+ if (value === "--id") {
1524
+ options.id = requireNextValue(values, index, "--id");
1525
+ index += 1;
1526
+ continue;
1527
+ }
1528
+
1529
+ if (value === "--decision") {
1530
+ options.decision = requireNextValue(values, index, "--decision");
1531
+ index += 1;
1532
+ continue;
1533
+ }
1534
+
1535
+ if (value === "--out") {
1536
+ options.outPath = requireNextValue(values, index, "--out");
1537
+ index += 1;
1538
+ continue;
1539
+ }
1540
+
1541
+ if (value === "--actor") {
1542
+ options.actor = requireNextValue(values, index, "--actor");
1543
+ index += 1;
1544
+ continue;
1545
+ }
1546
+
1547
+ if (value === "--reason") {
1548
+ options.reason = requireNextValue(values, index, "--reason");
1549
+ index += 1;
1550
+ continue;
1551
+ }
1552
+
1553
+ if (value.startsWith("--")) {
1554
+ throw new Error(`Unknown option: ${value}`);
1555
+ }
1556
+
1557
+ paths.push(value);
1558
+ }
1559
+
1560
+ options.approvalPath = paths[0];
1561
+
1562
+ if (!options.approvalPath) {
1563
+ throw new Error("approvals decide requires <approval.json|approvals.jsonl>.");
1564
+ }
1565
+
1566
+ if (!options.id) {
1567
+ throw new Error("approvals decide requires --id <id>.");
1568
+ }
1569
+
1570
+ if (!options.decision) {
1571
+ throw new Error("approvals decide requires --decision approve|deny.");
1572
+ }
1573
+
1574
+ options.decision = normalizeApprovalDecision(options.decision);
1575
+
1576
+ if (!["approve", "deny"].includes(options.decision)) {
1577
+ throw new Error("Invalid --decision value. Use one of: approve, deny");
1578
+ }
1579
+
1580
+ return options;
1581
+ }
1582
+
1396
1583
  async function writeReportFile(outputPath, content) {
1397
1584
  const resolvedPath = path.resolve(outputPath);
1398
1585
  await fs.mkdir(path.dirname(resolvedPath), { recursive: true });