@denial-web/clawguard 0.1.10 → 0.1.11
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 +18 -0
- package/package.json +1 -1
- package/src/cli.js +202 -1
package/README.md
CHANGED
|
@@ -114,6 +114,14 @@ TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals poll-telegra
|
|
|
114
114
|
|
|
115
115
|
The poller records the Telegram update offset in `./.clawguard/decisions.jsonl.telegram-state.json` by default so the same reply is not processed again.
|
|
116
116
|
|
|
117
|
+
Apply the recorded decision to continue or block the pending install:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <approval-id> --decisions ./.clawguard/decisions.jsonl
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If the latest decision is `approve`, ClawGuard copies the original scanned source to the original approved destination. If the latest decision is `deny`, it exits blocked without copying. If no decision exists yet, it stays paused.
|
|
124
|
+
|
|
117
125
|
When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
|
|
118
126
|
|
|
119
127
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -111,6 +111,24 @@ TELEGRAM_BOT_TOKEN=123456:token clawguard approvals poll-telegram ./.clawguard/a
|
|
|
111
111
|
|
|
112
112
|
The poller calls Telegram `getUpdates`, parses approval commands, looks up the referenced approval request, and appends a `clawguard.decision.v1` decision. It records the next Telegram update offset in `./.clawguard/decisions.jsonl.telegram-state.json` by default. For tests and offline replay, `--telegram-updates-file <path>` can read a captured Telegram-style update payload instead of calling the Telegram API.
|
|
113
113
|
|
|
114
|
+
### Approval Apply
|
|
115
|
+
|
|
116
|
+
After a decision is recorded, ClawGuard can apply that decision to the original pending install:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
clawguard approvals apply ./.clawguard/approvals.jsonl \
|
|
120
|
+
--id <approval-id> \
|
|
121
|
+
--decisions ./.clawguard/decisions.jsonl
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Apply behavior:
|
|
125
|
+
|
|
126
|
+
- If the latest matching decision is `approve`, copy the original scanned `target` to the original approval `destination`.
|
|
127
|
+
- If the latest matching decision is `deny`, exit blocked and copy nothing.
|
|
128
|
+
- If no matching decision exists, exit paused and copy nothing.
|
|
129
|
+
- Never execute the skill or install dependencies during apply.
|
|
130
|
+
- Refuse symlinked install sources and existing destinations, matching normal install safety.
|
|
131
|
+
|
|
114
132
|
### Skill Folder Scan
|
|
115
133
|
|
|
116
134
|
Command:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -40,7 +40,8 @@ if (![
|
|
|
40
40
|
"approvals-send",
|
|
41
41
|
"approvals-watch",
|
|
42
42
|
"approvals-decide",
|
|
43
|
-
"approvals-poll-telegram"
|
|
43
|
+
"approvals-poll-telegram",
|
|
44
|
+
"approvals-apply"
|
|
44
45
|
].includes(command)) {
|
|
45
46
|
console.error(`Unknown command: ${command}`);
|
|
46
47
|
printHelp();
|
|
@@ -94,6 +95,17 @@ try {
|
|
|
94
95
|
process.exit(0);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
if (command === "approvals-apply") {
|
|
99
|
+
const applyOptions = parseApprovalApplyOptions(optionValues);
|
|
100
|
+
const result = await applyApprovalDecision(applyOptions);
|
|
101
|
+
if (applyOptions.json) {
|
|
102
|
+
console.log(JSON.stringify(result, null, 2));
|
|
103
|
+
} else {
|
|
104
|
+
printApprovalApplyResult(result);
|
|
105
|
+
}
|
|
106
|
+
process.exit(approvalApplyExitCode(result));
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
const cliOptions = parseOptions(optionValues);
|
|
98
110
|
cliOptions.framework = framework;
|
|
99
111
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -158,6 +170,7 @@ Usage:
|
|
|
158
170
|
clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
|
|
159
171
|
clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
|
|
160
172
|
clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
|
|
173
|
+
clawguard approvals apply <approvals.jsonl> --id <id> --decisions <decisions.jsonl>
|
|
161
174
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
162
175
|
npm run scan -- <path>
|
|
163
176
|
|
|
@@ -218,6 +231,7 @@ Examples:
|
|
|
218
231
|
npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
219
232
|
npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
|
|
220
233
|
npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
|
|
234
|
+
npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <id> --decisions ./.clawguard/decisions.jsonl
|
|
221
235
|
npm run scan -- examples/risky-skill
|
|
222
236
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
223
237
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -333,6 +347,14 @@ function parseCommand(values) {
|
|
|
333
347
|
};
|
|
334
348
|
}
|
|
335
349
|
|
|
350
|
+
if (rawCommand === "approvals" && values[1] === "apply") {
|
|
351
|
+
return {
|
|
352
|
+
command: "approvals-apply",
|
|
353
|
+
framework: undefined,
|
|
354
|
+
optionValues: values.slice(2)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
336
358
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
337
359
|
const nestedCommand = values[1];
|
|
338
360
|
|
|
@@ -578,6 +600,97 @@ async function pollTelegramApprovals(options) {
|
|
|
578
600
|
return result;
|
|
579
601
|
}
|
|
580
602
|
|
|
603
|
+
async function applyApprovalDecision(options) {
|
|
604
|
+
const approval = await readApprovalRequest(options.approvalPath, options.id);
|
|
605
|
+
const decisionsPath = path.resolve(options.decisionsPath ?? `${options.approvalPath}.decisions.jsonl`);
|
|
606
|
+
const decision = await readLatestApprovalDecision(decisionsPath, approval.id);
|
|
607
|
+
const result = {
|
|
608
|
+
approval: {
|
|
609
|
+
id: approval.id,
|
|
610
|
+
status: approval.status,
|
|
611
|
+
decision: approval.decision,
|
|
612
|
+
risk: approval.risk,
|
|
613
|
+
framework: approval.framework
|
|
614
|
+
},
|
|
615
|
+
decision,
|
|
616
|
+
decisionsPath,
|
|
617
|
+
source: approval.target ? path.resolve(approval.target) : undefined,
|
|
618
|
+
destination: approval.destination ? path.resolve(approval.destination) : undefined,
|
|
619
|
+
dryRun: options.dryRun,
|
|
620
|
+
installed: false,
|
|
621
|
+
skipped: true,
|
|
622
|
+
reason: undefined
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
if (!decision) {
|
|
626
|
+
result.reason = "No decision has been recorded for this approval.";
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (decision.decision !== "approve") {
|
|
631
|
+
result.reason = decision.reason ?? "Approval was denied.";
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!result.source) {
|
|
636
|
+
throw new Error("Approval request has no target path to install.");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (!result.destination) {
|
|
640
|
+
throw new Error("Approval request has no destination path to install.");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (options.dryRun) {
|
|
644
|
+
result.skipped = false;
|
|
645
|
+
result.reason = "Dry run passed; no files were copied.";
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
await assertInstallableSource(result.source);
|
|
650
|
+
await assertDestinationAvailable(result.destination);
|
|
651
|
+
await fs.mkdir(path.dirname(result.destination), { recursive: true });
|
|
652
|
+
await fs.cp(result.source, result.destination, {
|
|
653
|
+
recursive: true,
|
|
654
|
+
errorOnExist: true,
|
|
655
|
+
force: false,
|
|
656
|
+
verbatimSymlinks: true
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
result.installed = true;
|
|
660
|
+
result.skipped = false;
|
|
661
|
+
result.reason = "Copied after recorded approval.";
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function readLatestApprovalDecision(decisionsPath, approvalId) {
|
|
666
|
+
let decisions;
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
decisions = await readApprovalDecisions(decisionsPath);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
if (error.code === "ENOENT") {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return decisions.filter((decision) => decision.approvalId === approvalId).at(-1);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function readApprovalDecisions(decisionsPath) {
|
|
682
|
+
const content = await fs.readFile(decisionsPath, "utf8");
|
|
683
|
+
const decisions = content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
684
|
+
|
|
685
|
+
for (const decision of decisions) {
|
|
686
|
+
if (decision.schemaVersion !== "clawguard.decision.v1") {
|
|
687
|
+
throw new Error("Unsupported approval decision schema.");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return decisions;
|
|
692
|
+
}
|
|
693
|
+
|
|
581
694
|
async function fetchTelegramUpdates(options, offset) {
|
|
582
695
|
if (options.telegramUpdatesPath) {
|
|
583
696
|
const content = await fs.readFile(path.resolve(options.telegramUpdatesPath), "utf8");
|
|
@@ -817,6 +930,17 @@ function printApprovalTelegramPollResult(result) {
|
|
|
817
930
|
}
|
|
818
931
|
}
|
|
819
932
|
|
|
933
|
+
function printApprovalApplyResult(result) {
|
|
934
|
+
console.log(`ClawGuard approval apply: ${result.approval.id}`);
|
|
935
|
+
console.log(`Decision: ${result.decision ? formatDecision(result.decision.decision) : "PENDING"}`);
|
|
936
|
+
console.log(`Source: ${result.source ?? "not recorded"}`);
|
|
937
|
+
console.log(`Destination: ${result.destination ?? "not recorded"}`);
|
|
938
|
+
console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
|
|
939
|
+
console.log(`Installed: ${result.installed ? "yes" : "no"}`);
|
|
940
|
+
console.log(`Exit code: ${approvalApplyExitCode(result)}`);
|
|
941
|
+
console.log(`Reason: ${result.reason}`);
|
|
942
|
+
}
|
|
943
|
+
|
|
820
944
|
async function readApprovalRequest(approvalPath, id) {
|
|
821
945
|
const resolvedPath = path.resolve(approvalPath);
|
|
822
946
|
const approvals = await readApprovalRequests(resolvedPath);
|
|
@@ -1246,6 +1370,10 @@ function commandLabel(commandName) {
|
|
|
1246
1370
|
return "Telegram approval poll";
|
|
1247
1371
|
}
|
|
1248
1372
|
|
|
1373
|
+
if (commandName === "approvals-apply") {
|
|
1374
|
+
return "Approval apply";
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1249
1377
|
if (commandName === "gate") {
|
|
1250
1378
|
return "Gate";
|
|
1251
1379
|
}
|
|
@@ -1324,6 +1452,18 @@ function installExitCode(decision, install) {
|
|
|
1324
1452
|
return gateExitCode(decision);
|
|
1325
1453
|
}
|
|
1326
1454
|
|
|
1455
|
+
function approvalApplyExitCode(result) {
|
|
1456
|
+
if (!result.decision) {
|
|
1457
|
+
return 1;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (result.decision.decision !== "approve") {
|
|
1461
|
+
return 2;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return 0;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1327
1467
|
function parseOptions(values) {
|
|
1328
1468
|
const options = {
|
|
1329
1469
|
json: false,
|
|
@@ -1893,6 +2033,67 @@ function parseApprovalTelegramPollOptions(values) {
|
|
|
1893
2033
|
return options;
|
|
1894
2034
|
}
|
|
1895
2035
|
|
|
2036
|
+
function parseApprovalApplyOptions(values) {
|
|
2037
|
+
const options = {
|
|
2038
|
+
approvalPath: undefined,
|
|
2039
|
+
id: undefined,
|
|
2040
|
+
decisionsPath: undefined,
|
|
2041
|
+
dryRun: false,
|
|
2042
|
+
json: false
|
|
2043
|
+
};
|
|
2044
|
+
const paths = [];
|
|
2045
|
+
|
|
2046
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
2047
|
+
const value = values[index];
|
|
2048
|
+
|
|
2049
|
+
if (value === "--json") {
|
|
2050
|
+
options.json = true;
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
if (value === "--dry-run") {
|
|
2055
|
+
options.dryRun = true;
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
if (value === "--id") {
|
|
2060
|
+
options.id = requireNextValue(values, index, "--id");
|
|
2061
|
+
index += 1;
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if (value === "--decisions") {
|
|
2066
|
+
options.decisionsPath = requireNextValue(values, index, "--decisions");
|
|
2067
|
+
index += 1;
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (value === "--out") {
|
|
2072
|
+
options.decisionsPath = requireNextValue(values, index, "--out");
|
|
2073
|
+
index += 1;
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
if (value.startsWith("--")) {
|
|
2078
|
+
throw new Error(`Unknown option: ${value}`);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
paths.push(value);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
options.approvalPath = paths[0];
|
|
2085
|
+
|
|
2086
|
+
if (!options.approvalPath) {
|
|
2087
|
+
throw new Error("approvals apply requires <approval.json|approvals.jsonl>.");
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
if (!options.id) {
|
|
2091
|
+
throw new Error("approvals apply requires --id <id>.");
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
return options;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1896
2097
|
async function writeReportFile(outputPath, content) {
|
|
1897
2098
|
const resolvedPath = path.resolve(outputPath);
|
|
1898
2099
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|