@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 +7 -0
- package/docs/INTEGRATION_SPEC.md +15 -0
- package/package.json +1 -1
- package/src/cli.js +188 -1
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
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -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
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 (![
|
|
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 });
|