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