@denial-web/clawguard 0.1.10 → 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 +16 -0
- package/docs/INTEGRATION_SPEC.md +29 -0
- package/package.json +1 -1
- package/src/cli.js +540 -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
|
|
@@ -114,6 +122,14 @@ TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals poll-telegra
|
|
|
114
122
|
|
|
115
123
|
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
124
|
|
|
125
|
+
Apply the recorded decision to continue or block the pending install:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <approval-id> --decisions ./.clawguard/decisions.jsonl
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
117
133
|
When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
|
|
118
134
|
|
|
119
135
|
```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:
|
|
@@ -111,6 +122,24 @@ TELEGRAM_BOT_TOKEN=123456:token clawguard approvals poll-telegram ./.clawguard/a
|
|
|
111
122
|
|
|
112
123
|
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
124
|
|
|
125
|
+
### Approval Apply
|
|
126
|
+
|
|
127
|
+
After a decision is recorded, ClawGuard can apply that decision to the original pending install:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
clawguard approvals apply ./.clawguard/approvals.jsonl \
|
|
131
|
+
--id <approval-id> \
|
|
132
|
+
--decisions ./.clawguard/decisions.jsonl
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Apply behavior:
|
|
136
|
+
|
|
137
|
+
- If the latest matching decision is `approve`, copy the original scanned `target` to the original approval `destination`.
|
|
138
|
+
- If the latest matching decision is `deny`, exit blocked and copy nothing.
|
|
139
|
+
- If no matching decision exists, exit paused and copy nothing.
|
|
140
|
+
- Never execute the skill or install dependencies during apply.
|
|
141
|
+
- Refuse symlinked install sources and existing destinations, matching normal install safety.
|
|
142
|
+
|
|
114
143
|
### Skill Folder Scan
|
|
115
144
|
|
|
116
145
|
Command:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -40,7 +40,9 @@ if (![
|
|
|
40
40
|
"approvals-send",
|
|
41
41
|
"approvals-watch",
|
|
42
42
|
"approvals-decide",
|
|
43
|
-
"approvals-poll-telegram"
|
|
43
|
+
"approvals-poll-telegram",
|
|
44
|
+
"approvals-apply",
|
|
45
|
+
"approvals-doctor"
|
|
44
46
|
].includes(command)) {
|
|
45
47
|
console.error(`Unknown command: ${command}`);
|
|
46
48
|
printHelp();
|
|
@@ -94,6 +96,28 @@ try {
|
|
|
94
96
|
process.exit(0);
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
if (command === "approvals-apply") {
|
|
100
|
+
const applyOptions = parseApprovalApplyOptions(optionValues);
|
|
101
|
+
const result = await applyApprovalDecision(applyOptions);
|
|
102
|
+
if (applyOptions.json) {
|
|
103
|
+
console.log(JSON.stringify(result, null, 2));
|
|
104
|
+
} else {
|
|
105
|
+
printApprovalApplyResult(result);
|
|
106
|
+
}
|
|
107
|
+
process.exit(approvalApplyExitCode(result));
|
|
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
|
+
|
|
97
121
|
const cliOptions = parseOptions(optionValues);
|
|
98
122
|
cliOptions.framework = framework;
|
|
99
123
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -158,6 +182,8 @@ Usage:
|
|
|
158
182
|
clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
|
|
159
183
|
clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
|
|
160
184
|
clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
|
|
185
|
+
clawguard approvals apply <approvals.jsonl> --id <id> --decisions <decisions.jsonl>
|
|
186
|
+
clawguard approvals doctor [--chat-id <id>]
|
|
161
187
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
162
188
|
npm run scan -- <path>
|
|
163
189
|
|
|
@@ -201,6 +227,8 @@ Options:
|
|
|
201
227
|
--offset-state <path> Telegram update offset state file.
|
|
202
228
|
--telegram-updates-file <path>
|
|
203
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.
|
|
204
232
|
|
|
205
233
|
Gate exit codes:
|
|
206
234
|
0 = allow
|
|
@@ -218,6 +246,8 @@ Examples:
|
|
|
218
246
|
npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
219
247
|
npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
|
|
220
248
|
npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
|
|
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
|
|
221
251
|
npm run scan -- examples/risky-skill
|
|
222
252
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
223
253
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -333,6 +363,22 @@ function parseCommand(values) {
|
|
|
333
363
|
};
|
|
334
364
|
}
|
|
335
365
|
|
|
366
|
+
if (rawCommand === "approvals" && values[1] === "apply") {
|
|
367
|
+
return {
|
|
368
|
+
command: "approvals-apply",
|
|
369
|
+
framework: undefined,
|
|
370
|
+
optionValues: values.slice(2)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
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
|
+
|
|
336
382
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
337
383
|
const nestedCommand = values[1];
|
|
338
384
|
|
|
@@ -578,6 +624,300 @@ async function pollTelegramApprovals(options) {
|
|
|
578
624
|
return result;
|
|
579
625
|
}
|
|
580
626
|
|
|
627
|
+
async function applyApprovalDecision(options) {
|
|
628
|
+
const approval = await readApprovalRequest(options.approvalPath, options.id);
|
|
629
|
+
const decisionsPath = path.resolve(options.decisionsPath ?? `${options.approvalPath}.decisions.jsonl`);
|
|
630
|
+
const decision = await readLatestApprovalDecision(decisionsPath, approval.id);
|
|
631
|
+
const result = {
|
|
632
|
+
approval: {
|
|
633
|
+
id: approval.id,
|
|
634
|
+
status: approval.status,
|
|
635
|
+
decision: approval.decision,
|
|
636
|
+
risk: approval.risk,
|
|
637
|
+
framework: approval.framework
|
|
638
|
+
},
|
|
639
|
+
decision,
|
|
640
|
+
decisionsPath,
|
|
641
|
+
source: approval.target ? path.resolve(approval.target) : undefined,
|
|
642
|
+
destination: approval.destination ? path.resolve(approval.destination) : undefined,
|
|
643
|
+
dryRun: options.dryRun,
|
|
644
|
+
installed: false,
|
|
645
|
+
skipped: true,
|
|
646
|
+
reason: undefined
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
if (!decision) {
|
|
650
|
+
result.reason = "No decision has been recorded for this approval.";
|
|
651
|
+
return result;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (decision.decision !== "approve") {
|
|
655
|
+
result.reason = decision.reason ?? "Approval was denied.";
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!result.source) {
|
|
660
|
+
throw new Error("Approval request has no target path to install.");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (!result.destination) {
|
|
664
|
+
throw new Error("Approval request has no destination path to install.");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (options.dryRun) {
|
|
668
|
+
result.skipped = false;
|
|
669
|
+
result.reason = "Dry run passed; no files were copied.";
|
|
670
|
+
return result;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
await assertInstallableSource(result.source);
|
|
674
|
+
await assertDestinationAvailable(result.destination);
|
|
675
|
+
await fs.mkdir(path.dirname(result.destination), { recursive: true });
|
|
676
|
+
await fs.cp(result.source, result.destination, {
|
|
677
|
+
recursive: true,
|
|
678
|
+
errorOnExist: true,
|
|
679
|
+
force: false,
|
|
680
|
+
verbatimSymlinks: true
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
result.installed = true;
|
|
684
|
+
result.skipped = false;
|
|
685
|
+
result.reason = "Copied after recorded approval.";
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
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
|
+
|
|
892
|
+
async function readLatestApprovalDecision(decisionsPath, approvalId) {
|
|
893
|
+
let decisions;
|
|
894
|
+
|
|
895
|
+
try {
|
|
896
|
+
decisions = await readApprovalDecisions(decisionsPath);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (error.code === "ENOENT") {
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return decisions.filter((decision) => decision.approvalId === approvalId).at(-1);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function readApprovalDecisions(decisionsPath) {
|
|
909
|
+
const content = await fs.readFile(decisionsPath, "utf8");
|
|
910
|
+
const decisions = content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
911
|
+
|
|
912
|
+
for (const decision of decisions) {
|
|
913
|
+
if (decision.schemaVersion !== "clawguard.decision.v1") {
|
|
914
|
+
throw new Error("Unsupported approval decision schema.");
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return decisions;
|
|
919
|
+
}
|
|
920
|
+
|
|
581
921
|
async function fetchTelegramUpdates(options, offset) {
|
|
582
922
|
if (options.telegramUpdatesPath) {
|
|
583
923
|
const content = await fs.readFile(path.resolve(options.telegramUpdatesPath), "utf8");
|
|
@@ -817,6 +1157,35 @@ function printApprovalTelegramPollResult(result) {
|
|
|
817
1157
|
}
|
|
818
1158
|
}
|
|
819
1159
|
|
|
1160
|
+
function printApprovalApplyResult(result) {
|
|
1161
|
+
console.log(`ClawGuard approval apply: ${result.approval.id}`);
|
|
1162
|
+
console.log(`Decision: ${result.decision ? formatDecision(result.decision.decision) : "PENDING"}`);
|
|
1163
|
+
console.log(`Source: ${result.source ?? "not recorded"}`);
|
|
1164
|
+
console.log(`Destination: ${result.destination ?? "not recorded"}`);
|
|
1165
|
+
console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
|
|
1166
|
+
console.log(`Installed: ${result.installed ? "yes" : "no"}`);
|
|
1167
|
+
console.log(`Exit code: ${approvalApplyExitCode(result)}`);
|
|
1168
|
+
console.log(`Reason: ${result.reason}`);
|
|
1169
|
+
}
|
|
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
|
+
|
|
820
1189
|
async function readApprovalRequest(approvalPath, id) {
|
|
821
1190
|
const resolvedPath = path.resolve(approvalPath);
|
|
822
1191
|
const approvals = await readApprovalRequests(resolvedPath);
|
|
@@ -1246,6 +1615,14 @@ function commandLabel(commandName) {
|
|
|
1246
1615
|
return "Telegram approval poll";
|
|
1247
1616
|
}
|
|
1248
1617
|
|
|
1618
|
+
if (commandName === "approvals-apply") {
|
|
1619
|
+
return "Approval apply";
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
if (commandName === "approvals-doctor") {
|
|
1623
|
+
return "Approvals doctor";
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1249
1626
|
if (commandName === "gate") {
|
|
1250
1627
|
return "Gate";
|
|
1251
1628
|
}
|
|
@@ -1324,6 +1701,18 @@ function installExitCode(decision, install) {
|
|
|
1324
1701
|
return gateExitCode(decision);
|
|
1325
1702
|
}
|
|
1326
1703
|
|
|
1704
|
+
function approvalApplyExitCode(result) {
|
|
1705
|
+
if (!result.decision) {
|
|
1706
|
+
return 1;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (result.decision.decision !== "approve") {
|
|
1710
|
+
return 2;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
return 0;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1327
1716
|
function parseOptions(values) {
|
|
1328
1717
|
const options = {
|
|
1329
1718
|
json: false,
|
|
@@ -1893,6 +2282,156 @@ function parseApprovalTelegramPollOptions(values) {
|
|
|
1893
2282
|
return options;
|
|
1894
2283
|
}
|
|
1895
2284
|
|
|
2285
|
+
function parseApprovalApplyOptions(values) {
|
|
2286
|
+
const options = {
|
|
2287
|
+
approvalPath: undefined,
|
|
2288
|
+
id: undefined,
|
|
2289
|
+
decisionsPath: undefined,
|
|
2290
|
+
dryRun: false,
|
|
2291
|
+
json: false
|
|
2292
|
+
};
|
|
2293
|
+
const paths = [];
|
|
2294
|
+
|
|
2295
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
2296
|
+
const value = values[index];
|
|
2297
|
+
|
|
2298
|
+
if (value === "--json") {
|
|
2299
|
+
options.json = true;
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
if (value === "--dry-run") {
|
|
2304
|
+
options.dryRun = true;
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
if (value === "--id") {
|
|
2309
|
+
options.id = requireNextValue(values, index, "--id");
|
|
2310
|
+
index += 1;
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
if (value === "--decisions") {
|
|
2315
|
+
options.decisionsPath = requireNextValue(values, index, "--decisions");
|
|
2316
|
+
index += 1;
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
if (value === "--out") {
|
|
2321
|
+
options.decisionsPath = requireNextValue(values, index, "--out");
|
|
2322
|
+
index += 1;
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
if (value.startsWith("--")) {
|
|
2327
|
+
throw new Error(`Unknown option: ${value}`);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
paths.push(value);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
options.approvalPath = paths[0];
|
|
2334
|
+
|
|
2335
|
+
if (!options.approvalPath) {
|
|
2336
|
+
throw new Error("approvals apply requires <approval.json|approvals.jsonl>.");
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
if (!options.id) {
|
|
2340
|
+
throw new Error("approvals apply requires --id <id>.");
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
return options;
|
|
2344
|
+
}
|
|
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
|
+
|
|
1896
2435
|
async function writeReportFile(outputPath, content) {
|
|
1897
2436
|
const resolvedPath = path.resolve(outputPath);
|
|
1898
2437
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|