@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 +8 -0
- package/docs/INTEGRATION_SPEC.md +11 -0
- package/package.json +1 -1
- package/src/cli.js +339 -1
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
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -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
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 });
|