@denial-web/clawguard 0.1.11 → 0.1.12

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,14 @@ 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
+ Check the approval setup and print the exact command flow:
72
+
73
+ ```bash
74
+ npx @denial-web/clawguard approvals doctor --chat-id 123456789
75
+ ```
76
+
77
+ Use `--framework hermes` to print Hermes install commands, or `--check-telegram` when you want ClawGuard to call Telegram `getMe` and verify the bot token.
78
+
71
79
  If OpenClaw already has messaging configured, ClawGuard can hand the approval message to OpenClaw:
72
80
 
73
81
  ```bash
@@ -78,6 +78,17 @@ 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 Doctor
82
+
83
+ Before wiring a real agent into the approval loop, users can run:
84
+
85
+ ```bash
86
+ clawguard approvals doctor \
87
+ --chat-id 123456789
88
+ ```
89
+
90
+ The doctor checks local runtime readiness, writable approval and decision paths, install destination writability, Telegram token presence, and Telegram chat id presence. It does not call Telegram by default. With `--check-telegram`, it calls Telegram `getMe` to verify the configured bot token. The command prints suggested guarded install, watcher, poller, and apply commands for OpenClaw by default; `--framework hermes` switches the guarded install example to Hermes.
91
+
81
92
  ### Approval Decisions
82
93
 
83
94
  Owner decisions are stored separately from approval requests. This keeps the original scan evidence immutable and gives messaging bridges a simple append-only target:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denial-web/clawguard",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
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
@@ -41,7 +41,8 @@ if (![
41
41
  "approvals-watch",
42
42
  "approvals-decide",
43
43
  "approvals-poll-telegram",
44
- "approvals-apply"
44
+ "approvals-apply",
45
+ "approvals-doctor"
45
46
  ].includes(command)) {
46
47
  console.error(`Unknown command: ${command}`);
47
48
  printHelp();
@@ -106,6 +107,17 @@ try {
106
107
  process.exit(approvalApplyExitCode(result));
107
108
  }
108
109
 
110
+ if (command === "approvals-doctor") {
111
+ const doctorOptions = parseApprovalDoctorOptions(optionValues);
112
+ const result = await runApprovalDoctor(doctorOptions);
113
+ if (doctorOptions.json) {
114
+ console.log(JSON.stringify(result, null, 2));
115
+ } else {
116
+ printApprovalDoctorResult(result);
117
+ }
118
+ process.exit(result.ok ? 0 : 1);
119
+ }
120
+
109
121
  const cliOptions = parseOptions(optionValues);
110
122
  cliOptions.framework = framework;
111
123
  const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
@@ -171,6 +183,7 @@ Usage:
171
183
  clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
172
184
  clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
173
185
  clawguard approvals apply <approvals.jsonl> --id <id> --decisions <decisions.jsonl>
186
+ clawguard approvals doctor [--chat-id <id>]
174
187
  clawguard scan-workspace <path> [--json] [--policy <preset>]
175
188
  npm run scan -- <path>
176
189
 
@@ -214,6 +227,8 @@ Options:
214
227
  --offset-state <path> Telegram update offset state file.
215
228
  --telegram-updates-file <path>
216
229
  Read Telegram updates from a JSON file for tests or offline replay.
230
+ --check-telegram In approvals doctor, call Telegram getMe to verify the bot token.
231
+ --framework <name> In approvals doctor, show openclaw or hermes commands. Default: openclaw.
217
232
 
218
233
  Gate exit codes:
219
234
  0 = allow
@@ -232,6 +247,7 @@ Examples:
232
247
  npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
233
248
  npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
234
249
  npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <id> --decisions ./.clawguard/decisions.jsonl
250
+ npx @denial-web/clawguard approvals doctor --chat-id 123456789
235
251
  npm run scan -- examples/risky-skill
236
252
  npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
237
253
  npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
@@ -355,6 +371,14 @@ function parseCommand(values) {
355
371
  };
356
372
  }
357
373
 
374
+ if (rawCommand === "approvals" && values[1] === "doctor") {
375
+ return {
376
+ command: "approvals-doctor",
377
+ framework: undefined,
378
+ optionValues: values.slice(2)
379
+ };
380
+ }
381
+
358
382
  if (["openclaw", "hermes"].includes(rawCommand)) {
359
383
  const nestedCommand = values[1];
360
384
 
@@ -662,6 +686,209 @@ async function applyApprovalDecision(options) {
662
686
  return result;
663
687
  }
664
688
 
689
+ async function runApprovalDoctor(options) {
690
+ const approvalPath = path.resolve(options.approvalPath);
691
+ const decisionsPath = path.resolve(options.decisionsPath);
692
+ const installDir = path.resolve(options.installDir);
693
+ const target = options.target;
694
+ const token = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
695
+ const checks = [
696
+ checkNodeVersion(),
697
+ {
698
+ id: "approval-path-format",
699
+ status: approvalPath.endsWith(".jsonl") ? "pass" : "warn",
700
+ message: approvalPath.endsWith(".jsonl")
701
+ ? "Approval queue uses JSONL."
702
+ : "Approval queue is not .jsonl; JSONL is recommended for watcher integrations.",
703
+ detail: approvalPath
704
+ },
705
+ {
706
+ id: "telegram-token",
707
+ status: token ? "pass" : "warn",
708
+ message: token
709
+ ? "Telegram bot token is configured."
710
+ : "Telegram bot token is not configured. Set TELEGRAM_BOT_TOKEN or pass --bot-token.",
711
+ detail: token ? "present" : "missing"
712
+ },
713
+ {
714
+ id: "telegram-chat",
715
+ status: options.chatId ? "pass" : "warn",
716
+ message: options.chatId
717
+ ? "Telegram chat id is configured."
718
+ : "Telegram chat id is missing. Pass --chat-id before running the watcher.",
719
+ detail: options.chatId ?? "missing"
720
+ }
721
+ ];
722
+
723
+ checks.push(await checkWritablePath("approval-directory-writable", path.dirname(approvalPath)));
724
+ checks.push(await checkWritablePath("decision-directory-writable", path.dirname(decisionsPath)));
725
+ checks.push(await checkWritablePath("install-directory-writable", installDir));
726
+
727
+ if (options.checkTelegram) {
728
+ checks.push(await checkTelegramBot(token, options));
729
+ }
730
+
731
+ const commands = createApprovalDoctorCommands({
732
+ framework: options.framework,
733
+ target,
734
+ installDir,
735
+ approvalPath,
736
+ decisionsPath,
737
+ chatId: options.chatId ?? "<telegram-chat-id>"
738
+ });
739
+ const ok = checks.every((check) => check.status !== "fail");
740
+
741
+ return {
742
+ ok,
743
+ framework: options.framework,
744
+ paths: {
745
+ target,
746
+ installDir,
747
+ approvalPath,
748
+ decisionsPath
749
+ },
750
+ checks,
751
+ commands
752
+ };
753
+ }
754
+
755
+ function checkNodeVersion() {
756
+ const major = Number.parseInt(process.versions.node.split(".")[0], 10);
757
+ return {
758
+ id: "node-version",
759
+ status: major >= 20 ? "pass" : "fail",
760
+ message: major >= 20
761
+ ? `Node.js ${process.versions.node} satisfies ClawGuard's runtime requirement.`
762
+ : `Node.js ${process.versions.node} is too old. ClawGuard requires Node.js 20 or newer.`,
763
+ detail: process.versions.node
764
+ };
765
+ }
766
+
767
+ async function checkWritablePath(id, directory) {
768
+ const resolved = path.resolve(directory);
769
+ const probePath = path.join(resolved, `.clawguard-doctor-${process.pid}.tmp`);
770
+
771
+ try {
772
+ await fs.mkdir(resolved, { recursive: true });
773
+ await fs.writeFile(probePath, "ok\n", { flag: "wx" });
774
+ await fs.unlink(probePath);
775
+ return {
776
+ id,
777
+ status: "pass",
778
+ message: "Directory is writable.",
779
+ detail: resolved
780
+ };
781
+ } catch (error) {
782
+ return {
783
+ id,
784
+ status: "fail",
785
+ message: `Directory is not writable: ${error.message}`,
786
+ detail: resolved
787
+ };
788
+ }
789
+ }
790
+
791
+ async function checkTelegramBot(botToken, options) {
792
+ if (!botToken) {
793
+ return {
794
+ id: "telegram-api",
795
+ status: "warn",
796
+ message: "Skipped Telegram API check because no bot token is configured.",
797
+ detail: "missing token"
798
+ };
799
+ }
800
+
801
+ const apiBase = options.telegramApiBase ?? "https://api.telegram.org";
802
+ const endpoint = `${apiBase.replace(/\/$/, "")}/bot${botToken}/getMe`;
803
+
804
+ try {
805
+ const response = await fetch(endpoint);
806
+ const text = await response.text();
807
+ let payload;
808
+
809
+ try {
810
+ payload = text ? JSON.parse(text) : null;
811
+ } catch {
812
+ payload = undefined;
813
+ }
814
+
815
+ if (!response.ok || payload?.ok === false) {
816
+ return {
817
+ id: "telegram-api",
818
+ status: "fail",
819
+ message: `Telegram getMe failed with HTTP ${response.status}.`,
820
+ detail: redactTelegramToken(endpoint)
821
+ };
822
+ }
823
+
824
+ return {
825
+ id: "telegram-api",
826
+ status: "pass",
827
+ message: "Telegram bot API responded successfully.",
828
+ detail: payload?.result?.username ? `@${payload.result.username}` : redactTelegramToken(endpoint)
829
+ };
830
+ } catch (error) {
831
+ return {
832
+ id: "telegram-api",
833
+ status: "fail",
834
+ message: `Telegram getMe failed: ${error.message}`,
835
+ detail: redactTelegramToken(endpoint)
836
+ };
837
+ }
838
+ }
839
+
840
+ function createApprovalDoctorCommands(details) {
841
+ const installArgs = [
842
+ "npx",
843
+ "@denial-web/clawguard",
844
+ details.framework,
845
+ "install",
846
+ details.target,
847
+ "--to",
848
+ details.installDir,
849
+ "--approval-out",
850
+ details.approvalPath
851
+ ];
852
+ const watchArgs = [
853
+ "npx",
854
+ "@denial-web/clawguard",
855
+ "approvals",
856
+ "watch",
857
+ details.approvalPath,
858
+ "--via",
859
+ "telegram",
860
+ "--chat-id",
861
+ details.chatId
862
+ ];
863
+ const pollArgs = [
864
+ "npx",
865
+ "@denial-web/clawguard",
866
+ "approvals",
867
+ "poll-telegram",
868
+ details.approvalPath,
869
+ "--decisions",
870
+ details.decisionsPath
871
+ ];
872
+ const applyArgs = [
873
+ "npx",
874
+ "@denial-web/clawguard",
875
+ "approvals",
876
+ "apply",
877
+ details.approvalPath,
878
+ "--id",
879
+ "<approval-id>",
880
+ "--decisions",
881
+ details.decisionsPath
882
+ ];
883
+
884
+ return {
885
+ guardedInstall: installArgs.map(shellQuote).join(" "),
886
+ watchTelegram: `TELEGRAM_BOT_TOKEN=<token> ${watchArgs.map(shellQuote).join(" ")}`,
887
+ pollTelegram: `TELEGRAM_BOT_TOKEN=<token> ${pollArgs.map(shellQuote).join(" ")}`,
888
+ applyDecision: applyArgs.map(shellQuote).join(" ")
889
+ };
890
+ }
891
+
665
892
  async function readLatestApprovalDecision(decisionsPath, approvalId) {
666
893
  let decisions;
667
894
 
@@ -941,6 +1168,24 @@ function printApprovalApplyResult(result) {
941
1168
  console.log(`Reason: ${result.reason}`);
942
1169
  }
943
1170
 
1171
+ function printApprovalDoctorResult(result) {
1172
+ console.log("ClawGuard approvals doctor");
1173
+ console.log(`Framework: ${displayFramework(result.framework)}`);
1174
+ console.log(`Ready: ${result.ok ? "yes" : "no"}`);
1175
+ console.log("\nChecks:");
1176
+ for (const check of result.checks) {
1177
+ console.log(`- [${check.status.toUpperCase()}] ${check.message}`);
1178
+ if (check.detail) {
1179
+ console.log(` ${check.detail}`);
1180
+ }
1181
+ }
1182
+ console.log("\nSuggested commands:");
1183
+ console.log(`1. ${result.commands.guardedInstall}`);
1184
+ console.log(`2. ${result.commands.watchTelegram}`);
1185
+ console.log(`3. ${result.commands.pollTelegram}`);
1186
+ console.log(`4. ${result.commands.applyDecision}`);
1187
+ }
1188
+
944
1189
  async function readApprovalRequest(approvalPath, id) {
945
1190
  const resolvedPath = path.resolve(approvalPath);
946
1191
  const approvals = await readApprovalRequests(resolvedPath);
@@ -1374,6 +1619,10 @@ function commandLabel(commandName) {
1374
1619
  return "Approval apply";
1375
1620
  }
1376
1621
 
1622
+ if (commandName === "approvals-doctor") {
1623
+ return "Approvals doctor";
1624
+ }
1625
+
1377
1626
  if (commandName === "gate") {
1378
1627
  return "Gate";
1379
1628
  }
@@ -2094,6 +2343,95 @@ function parseApprovalApplyOptions(values) {
2094
2343
  return options;
2095
2344
  }
2096
2345
 
2346
+ function parseApprovalDoctorOptions(values) {
2347
+ const options = {
2348
+ approvalPath: ".clawguard/approvals.jsonl",
2349
+ decisionsPath: ".clawguard/decisions.jsonl",
2350
+ installDir: ".agents/skills",
2351
+ target: "./candidate-skill",
2352
+ framework: "openclaw",
2353
+ chatId: undefined,
2354
+ botToken: undefined,
2355
+ telegramApiBase: undefined,
2356
+ checkTelegram: false,
2357
+ json: false
2358
+ };
2359
+
2360
+ for (let index = 0; index < values.length; index += 1) {
2361
+ const value = values[index];
2362
+
2363
+ if (value === "--json") {
2364
+ options.json = true;
2365
+ continue;
2366
+ }
2367
+
2368
+ if (value === "--approval-out") {
2369
+ options.approvalPath = requireNextValue(values, index, "--approval-out");
2370
+ index += 1;
2371
+ continue;
2372
+ }
2373
+
2374
+ if (value === "--decisions") {
2375
+ options.decisionsPath = requireNextValue(values, index, "--decisions");
2376
+ index += 1;
2377
+ continue;
2378
+ }
2379
+
2380
+ if (value === "--to") {
2381
+ options.installDir = requireNextValue(values, index, "--to");
2382
+ index += 1;
2383
+ continue;
2384
+ }
2385
+
2386
+ if (value === "--target") {
2387
+ options.target = requireNextValue(values, index, "--target");
2388
+ index += 1;
2389
+ continue;
2390
+ }
2391
+
2392
+ if (value === "--framework") {
2393
+ options.framework = requireNextValue(values, index, "--framework");
2394
+ index += 1;
2395
+ continue;
2396
+ }
2397
+
2398
+ if (value === "--chat-id") {
2399
+ options.chatId = requireNextValue(values, index, "--chat-id");
2400
+ index += 1;
2401
+ continue;
2402
+ }
2403
+
2404
+ if (value === "--bot-token") {
2405
+ options.botToken = requireNextValue(values, index, "--bot-token");
2406
+ index += 1;
2407
+ continue;
2408
+ }
2409
+
2410
+ if (value === "--telegram-api-base") {
2411
+ options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
2412
+ index += 1;
2413
+ continue;
2414
+ }
2415
+
2416
+ if (value === "--check-telegram") {
2417
+ options.checkTelegram = true;
2418
+ continue;
2419
+ }
2420
+
2421
+ if (value.startsWith("--")) {
2422
+ throw new Error(`Unknown option: ${value}`);
2423
+ }
2424
+
2425
+ throw new Error(`Unexpected argument for approvals doctor: ${value}`);
2426
+ }
2427
+
2428
+ if (!["openclaw", "hermes"].includes(options.framework)) {
2429
+ throw new Error("Invalid --framework value. Use one of: openclaw, hermes");
2430
+ }
2431
+
2432
+ return options;
2433
+ }
2434
+
2097
2435
  async function writeReportFile(outputPath, content) {
2098
2436
  const resolvedPath = path.resolve(outputPath);
2099
2437
  await fs.mkdir(path.dirname(resolvedPath), { recursive: true });