@denial-web/clawguard 0.1.11 → 0.1.13
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 +21 -0
- package/package.json +1 -1
- package/src/cli.js +591 -1
package/README.md
CHANGED
|
@@ -68,6 +68,22 @@ 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
|
+
To prove the full approval loop locally without Telegram, WhatsApp, OpenClaw, or Hermes credentials, run:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx @denial-web/clawguard approvals demo-flow --keep
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The demo creates a harmless temporary skill, writes a pending approval, records a local owner approval, applies the decision, and installs the skill into a temporary trusted folder. Remove `--keep` when you want ClawGuard to clean up the temporary workspace automatically.
|
|
78
|
+
|
|
79
|
+
Check the approval setup and print the exact command flow:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx @denial-web/clawguard approvals doctor --chat-id 123456789
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Use `--framework hermes` to print Hermes install commands, or `--check-telegram` when you want ClawGuard to call Telegram `getMe` and verify the bot token.
|
|
86
|
+
|
|
71
87
|
If OpenClaw already has messaging configured, ClawGuard can hand the approval message to OpenClaw:
|
|
72
88
|
|
|
73
89
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -78,6 +78,27 @@ 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
|
+
### Local Approval Demo
|
|
82
|
+
|
|
83
|
+
For demos, onboarding, and smoke tests, users can prove the full approval loop without OpenClaw, Hermes, Telegram, WhatsApp, or Slack credentials:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
clawguard approvals demo-flow --keep
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The demo creates a harmless temporary skill, scans it with the governed policy, forces an approval request with `--approval-mode always`, writes a local `approve` decision, applies that decision, and copies the skill into a temporary trusted folder. By default it removes the temporary workspace after the run. Use `--keep` when recording a demo or inspecting the generated approval and decision logs.
|
|
90
|
+
|
|
91
|
+
### Approval Doctor
|
|
92
|
+
|
|
93
|
+
Before wiring a real agent into the approval loop, users can run:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
clawguard approvals doctor \
|
|
97
|
+
--chat-id 123456789
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
81
102
|
### Approval Decisions
|
|
82
103
|
|
|
83
104
|
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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { promises as fs } from "node:fs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { promisify } from "node:util";
|
|
8
9
|
import { loadConfig, mergeConfig, parseSize } from "./config.js";
|
|
@@ -41,7 +42,9 @@ if (![
|
|
|
41
42
|
"approvals-watch",
|
|
42
43
|
"approvals-decide",
|
|
43
44
|
"approvals-poll-telegram",
|
|
44
|
-
"approvals-apply"
|
|
45
|
+
"approvals-apply",
|
|
46
|
+
"approvals-doctor",
|
|
47
|
+
"approvals-demo-flow"
|
|
45
48
|
].includes(command)) {
|
|
46
49
|
console.error(`Unknown command: ${command}`);
|
|
47
50
|
printHelp();
|
|
@@ -106,6 +109,28 @@ try {
|
|
|
106
109
|
process.exit(approvalApplyExitCode(result));
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
if (command === "approvals-doctor") {
|
|
113
|
+
const doctorOptions = parseApprovalDoctorOptions(optionValues);
|
|
114
|
+
const result = await runApprovalDoctor(doctorOptions);
|
|
115
|
+
if (doctorOptions.json) {
|
|
116
|
+
console.log(JSON.stringify(result, null, 2));
|
|
117
|
+
} else {
|
|
118
|
+
printApprovalDoctorResult(result);
|
|
119
|
+
}
|
|
120
|
+
process.exit(result.ok ? 0 : 1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (command === "approvals-demo-flow") {
|
|
124
|
+
const demoOptions = parseApprovalDemoFlowOptions(optionValues);
|
|
125
|
+
const result = await runApprovalDemoFlow(demoOptions);
|
|
126
|
+
if (demoOptions.json) {
|
|
127
|
+
console.log(JSON.stringify(result, null, 2));
|
|
128
|
+
} else {
|
|
129
|
+
printApprovalDemoFlowResult(result);
|
|
130
|
+
}
|
|
131
|
+
process.exit(result.ok ? 0 : 1);
|
|
132
|
+
}
|
|
133
|
+
|
|
109
134
|
const cliOptions = parseOptions(optionValues);
|
|
110
135
|
cliOptions.framework = framework;
|
|
111
136
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -171,6 +196,8 @@ Usage:
|
|
|
171
196
|
clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
|
|
172
197
|
clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
|
|
173
198
|
clawguard approvals apply <approvals.jsonl> --id <id> --decisions <decisions.jsonl>
|
|
199
|
+
clawguard approvals doctor [--chat-id <id>]
|
|
200
|
+
clawguard approvals demo-flow [--keep]
|
|
174
201
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
175
202
|
npm run scan -- <path>
|
|
176
203
|
|
|
@@ -214,6 +241,10 @@ Options:
|
|
|
214
241
|
--offset-state <path> Telegram update offset state file.
|
|
215
242
|
--telegram-updates-file <path>
|
|
216
243
|
Read Telegram updates from a JSON file for tests or offline replay.
|
|
244
|
+
--check-telegram In approvals doctor, call Telegram getMe to verify the bot token.
|
|
245
|
+
--framework <name> In approvals doctor, show openclaw or hermes commands. Default: openclaw.
|
|
246
|
+
In approvals demo-flow, label the demo as openclaw or hermes.
|
|
247
|
+
--keep In approvals demo-flow, keep the temporary demo workspace.
|
|
217
248
|
|
|
218
249
|
Gate exit codes:
|
|
219
250
|
0 = allow
|
|
@@ -232,6 +263,8 @@ Examples:
|
|
|
232
263
|
npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
|
|
233
264
|
npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
|
|
234
265
|
npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <id> --decisions ./.clawguard/decisions.jsonl
|
|
266
|
+
npx @denial-web/clawguard approvals doctor --chat-id 123456789
|
|
267
|
+
npx @denial-web/clawguard approvals demo-flow --keep
|
|
235
268
|
npm run scan -- examples/risky-skill
|
|
236
269
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
237
270
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -355,6 +388,22 @@ function parseCommand(values) {
|
|
|
355
388
|
};
|
|
356
389
|
}
|
|
357
390
|
|
|
391
|
+
if (rawCommand === "approvals" && values[1] === "doctor") {
|
|
392
|
+
return {
|
|
393
|
+
command: "approvals-doctor",
|
|
394
|
+
framework: undefined,
|
|
395
|
+
optionValues: values.slice(2)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (rawCommand === "approvals" && values[1] === "demo-flow") {
|
|
400
|
+
return {
|
|
401
|
+
command: "approvals-demo-flow",
|
|
402
|
+
framework: undefined,
|
|
403
|
+
optionValues: values.slice(2)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
358
407
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
359
408
|
const nestedCommand = values[1];
|
|
360
409
|
|
|
@@ -662,6 +711,357 @@ async function applyApprovalDecision(options) {
|
|
|
662
711
|
return result;
|
|
663
712
|
}
|
|
664
713
|
|
|
714
|
+
async function runApprovalDoctor(options) {
|
|
715
|
+
const approvalPath = path.resolve(options.approvalPath);
|
|
716
|
+
const decisionsPath = path.resolve(options.decisionsPath);
|
|
717
|
+
const installDir = path.resolve(options.installDir);
|
|
718
|
+
const target = options.target;
|
|
719
|
+
const token = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
720
|
+
const checks = [
|
|
721
|
+
checkNodeVersion(),
|
|
722
|
+
{
|
|
723
|
+
id: "approval-path-format",
|
|
724
|
+
status: approvalPath.endsWith(".jsonl") ? "pass" : "warn",
|
|
725
|
+
message: approvalPath.endsWith(".jsonl")
|
|
726
|
+
? "Approval queue uses JSONL."
|
|
727
|
+
: "Approval queue is not .jsonl; JSONL is recommended for watcher integrations.",
|
|
728
|
+
detail: approvalPath
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
id: "telegram-token",
|
|
732
|
+
status: token ? "pass" : "warn",
|
|
733
|
+
message: token
|
|
734
|
+
? "Telegram bot token is configured."
|
|
735
|
+
: "Telegram bot token is not configured. Set TELEGRAM_BOT_TOKEN or pass --bot-token.",
|
|
736
|
+
detail: token ? "present" : "missing"
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
id: "telegram-chat",
|
|
740
|
+
status: options.chatId ? "pass" : "warn",
|
|
741
|
+
message: options.chatId
|
|
742
|
+
? "Telegram chat id is configured."
|
|
743
|
+
: "Telegram chat id is missing. Pass --chat-id before running the watcher.",
|
|
744
|
+
detail: options.chatId ?? "missing"
|
|
745
|
+
}
|
|
746
|
+
];
|
|
747
|
+
|
|
748
|
+
checks.push(await checkWritablePath("approval-directory-writable", path.dirname(approvalPath)));
|
|
749
|
+
checks.push(await checkWritablePath("decision-directory-writable", path.dirname(decisionsPath)));
|
|
750
|
+
checks.push(await checkWritablePath("install-directory-writable", installDir));
|
|
751
|
+
|
|
752
|
+
if (options.checkTelegram) {
|
|
753
|
+
checks.push(await checkTelegramBot(token, options));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const commands = createApprovalDoctorCommands({
|
|
757
|
+
framework: options.framework,
|
|
758
|
+
target,
|
|
759
|
+
installDir,
|
|
760
|
+
approvalPath,
|
|
761
|
+
decisionsPath,
|
|
762
|
+
chatId: options.chatId ?? "<telegram-chat-id>"
|
|
763
|
+
});
|
|
764
|
+
const ok = checks.every((check) => check.status !== "fail");
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
ok,
|
|
768
|
+
framework: options.framework,
|
|
769
|
+
paths: {
|
|
770
|
+
target,
|
|
771
|
+
installDir,
|
|
772
|
+
approvalPath,
|
|
773
|
+
decisionsPath
|
|
774
|
+
},
|
|
775
|
+
checks,
|
|
776
|
+
commands
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function runApprovalDemoFlow(options) {
|
|
781
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), "clawguard-demo-flow-"));
|
|
782
|
+
const candidatePath = path.join(workspace, "candidate-skill");
|
|
783
|
+
const installDir = path.join(workspace, "trusted-skills");
|
|
784
|
+
const approvalPath = path.join(workspace, ".clawguard", "approvals.jsonl");
|
|
785
|
+
const decisionsPath = path.join(workspace, ".clawguard", "decisions.jsonl");
|
|
786
|
+
const steps = [];
|
|
787
|
+
|
|
788
|
+
await fs.mkdir(candidatePath, { recursive: true });
|
|
789
|
+
await fs.writeFile(path.join(candidatePath, "SKILL.md"), [
|
|
790
|
+
"# ClawGuard Demo Skill",
|
|
791
|
+
"",
|
|
792
|
+
"A harmless local skill used to prove the approval gate flow.",
|
|
793
|
+
"",
|
|
794
|
+
"It does not execute code, fetch network resources, or install dependencies.",
|
|
795
|
+
""
|
|
796
|
+
].join("\n"));
|
|
797
|
+
steps.push({
|
|
798
|
+
name: "create-demo-skill",
|
|
799
|
+
status: "pass",
|
|
800
|
+
detail: candidatePath
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
const scan = await scanTarget(candidatePath, {
|
|
804
|
+
policy: options.policy
|
|
805
|
+
});
|
|
806
|
+
steps.push({
|
|
807
|
+
name: "scan",
|
|
808
|
+
status: "pass",
|
|
809
|
+
detail: `${formatDecision(scan.policy.decision)} / ${scan.level.toUpperCase()} (${scan.score}/100)`
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
const install = await handleInstall(scan, {
|
|
813
|
+
target: candidatePath,
|
|
814
|
+
installDir,
|
|
815
|
+
installName: "demo-skill",
|
|
816
|
+
dryRun: false,
|
|
817
|
+
approvalOut: approvalPath,
|
|
818
|
+
approvalMode: "always",
|
|
819
|
+
framework: options.framework
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
if (!install.approvalRequest) {
|
|
823
|
+
throw new Error("Demo flow expected an approval request but none was created.");
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
steps.push({
|
|
827
|
+
name: "write-approval",
|
|
828
|
+
status: "pass",
|
|
829
|
+
detail: install.approvalRequest.id
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
const approval = await readApprovalRequest(approvalPath, install.approvalRequest.id);
|
|
833
|
+
const decisionResult = await decideApproval({
|
|
834
|
+
approvalPath,
|
|
835
|
+
id: approval.id,
|
|
836
|
+
decision: "approve",
|
|
837
|
+
outPath: decisionsPath,
|
|
838
|
+
actor: "clawguard-demo-flow",
|
|
839
|
+
reason: "Local demo approval.",
|
|
840
|
+
json: false
|
|
841
|
+
});
|
|
842
|
+
steps.push({
|
|
843
|
+
name: "record-owner-decision",
|
|
844
|
+
status: "pass",
|
|
845
|
+
detail: formatDecision(decisionResult.decision.decision)
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
const apply = await applyApprovalDecision({
|
|
849
|
+
approvalPath,
|
|
850
|
+
id: approval.id,
|
|
851
|
+
decisionsPath,
|
|
852
|
+
dryRun: false,
|
|
853
|
+
json: false
|
|
854
|
+
});
|
|
855
|
+
steps.push({
|
|
856
|
+
name: "apply-decision",
|
|
857
|
+
status: apply.installed ? "pass" : "fail",
|
|
858
|
+
detail: apply.reason
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const installedSkillPath = path.join(install.destination, "SKILL.md");
|
|
862
|
+
const installedSkill = await fs.readFile(installedSkillPath, "utf8");
|
|
863
|
+
const result = {
|
|
864
|
+
ok: apply.installed && installedSkill.includes("ClawGuard Demo Skill"),
|
|
865
|
+
cleanedUp: false,
|
|
866
|
+
kept: options.keep,
|
|
867
|
+
framework: options.framework,
|
|
868
|
+
policy: options.policy,
|
|
869
|
+
workspace,
|
|
870
|
+
paths: {
|
|
871
|
+
candidate: candidatePath,
|
|
872
|
+
installDir,
|
|
873
|
+
destination: install.destination,
|
|
874
|
+
installedSkill: installedSkillPath,
|
|
875
|
+
approvalPath,
|
|
876
|
+
decisionsPath
|
|
877
|
+
},
|
|
878
|
+
scan: {
|
|
879
|
+
decision: scan.policy.decision,
|
|
880
|
+
risk: {
|
|
881
|
+
level: scan.level,
|
|
882
|
+
score: scan.score
|
|
883
|
+
},
|
|
884
|
+
findings: scan.findings.length
|
|
885
|
+
},
|
|
886
|
+
approval: {
|
|
887
|
+
id: approval.id,
|
|
888
|
+
status: approval.status,
|
|
889
|
+
decision: approval.decision
|
|
890
|
+
},
|
|
891
|
+
decision: {
|
|
892
|
+
id: decisionResult.decision.id,
|
|
893
|
+
decision: decisionResult.decision.decision,
|
|
894
|
+
status: decisionResult.decision.status,
|
|
895
|
+
actor: decisionResult.decision.actor
|
|
896
|
+
},
|
|
897
|
+
apply: {
|
|
898
|
+
installed: apply.installed,
|
|
899
|
+
skipped: apply.skipped,
|
|
900
|
+
reason: apply.reason
|
|
901
|
+
},
|
|
902
|
+
steps
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
if (!options.keep) {
|
|
906
|
+
try {
|
|
907
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
908
|
+
result.cleanedUp = true;
|
|
909
|
+
steps.push({
|
|
910
|
+
name: "cleanup",
|
|
911
|
+
status: "pass",
|
|
912
|
+
detail: "Temporary workspace removed."
|
|
913
|
+
});
|
|
914
|
+
} catch (error) {
|
|
915
|
+
result.ok = false;
|
|
916
|
+
result.cleanupError = error.message;
|
|
917
|
+
steps.push({
|
|
918
|
+
name: "cleanup",
|
|
919
|
+
status: "fail",
|
|
920
|
+
detail: error.message
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return result;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function checkNodeVersion() {
|
|
929
|
+
const major = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
930
|
+
return {
|
|
931
|
+
id: "node-version",
|
|
932
|
+
status: major >= 20 ? "pass" : "fail",
|
|
933
|
+
message: major >= 20
|
|
934
|
+
? `Node.js ${process.versions.node} satisfies ClawGuard's runtime requirement.`
|
|
935
|
+
: `Node.js ${process.versions.node} is too old. ClawGuard requires Node.js 20 or newer.`,
|
|
936
|
+
detail: process.versions.node
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async function checkWritablePath(id, directory) {
|
|
941
|
+
const resolved = path.resolve(directory);
|
|
942
|
+
const probePath = path.join(resolved, `.clawguard-doctor-${process.pid}.tmp`);
|
|
943
|
+
|
|
944
|
+
try {
|
|
945
|
+
await fs.mkdir(resolved, { recursive: true });
|
|
946
|
+
await fs.writeFile(probePath, "ok\n", { flag: "wx" });
|
|
947
|
+
await fs.unlink(probePath);
|
|
948
|
+
return {
|
|
949
|
+
id,
|
|
950
|
+
status: "pass",
|
|
951
|
+
message: "Directory is writable.",
|
|
952
|
+
detail: resolved
|
|
953
|
+
};
|
|
954
|
+
} catch (error) {
|
|
955
|
+
return {
|
|
956
|
+
id,
|
|
957
|
+
status: "fail",
|
|
958
|
+
message: `Directory is not writable: ${error.message}`,
|
|
959
|
+
detail: resolved
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function checkTelegramBot(botToken, options) {
|
|
965
|
+
if (!botToken) {
|
|
966
|
+
return {
|
|
967
|
+
id: "telegram-api",
|
|
968
|
+
status: "warn",
|
|
969
|
+
message: "Skipped Telegram API check because no bot token is configured.",
|
|
970
|
+
detail: "missing token"
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const apiBase = options.telegramApiBase ?? "https://api.telegram.org";
|
|
975
|
+
const endpoint = `${apiBase.replace(/\/$/, "")}/bot${botToken}/getMe`;
|
|
976
|
+
|
|
977
|
+
try {
|
|
978
|
+
const response = await fetch(endpoint);
|
|
979
|
+
const text = await response.text();
|
|
980
|
+
let payload;
|
|
981
|
+
|
|
982
|
+
try {
|
|
983
|
+
payload = text ? JSON.parse(text) : null;
|
|
984
|
+
} catch {
|
|
985
|
+
payload = undefined;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (!response.ok || payload?.ok === false) {
|
|
989
|
+
return {
|
|
990
|
+
id: "telegram-api",
|
|
991
|
+
status: "fail",
|
|
992
|
+
message: `Telegram getMe failed with HTTP ${response.status}.`,
|
|
993
|
+
detail: redactTelegramToken(endpoint)
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return {
|
|
998
|
+
id: "telegram-api",
|
|
999
|
+
status: "pass",
|
|
1000
|
+
message: "Telegram bot API responded successfully.",
|
|
1001
|
+
detail: payload?.result?.username ? `@${payload.result.username}` : redactTelegramToken(endpoint)
|
|
1002
|
+
};
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
return {
|
|
1005
|
+
id: "telegram-api",
|
|
1006
|
+
status: "fail",
|
|
1007
|
+
message: `Telegram getMe failed: ${error.message}`,
|
|
1008
|
+
detail: redactTelegramToken(endpoint)
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function createApprovalDoctorCommands(details) {
|
|
1014
|
+
const installArgs = [
|
|
1015
|
+
"npx",
|
|
1016
|
+
"@denial-web/clawguard",
|
|
1017
|
+
details.framework,
|
|
1018
|
+
"install",
|
|
1019
|
+
details.target,
|
|
1020
|
+
"--to",
|
|
1021
|
+
details.installDir,
|
|
1022
|
+
"--approval-out",
|
|
1023
|
+
details.approvalPath
|
|
1024
|
+
];
|
|
1025
|
+
const watchArgs = [
|
|
1026
|
+
"npx",
|
|
1027
|
+
"@denial-web/clawguard",
|
|
1028
|
+
"approvals",
|
|
1029
|
+
"watch",
|
|
1030
|
+
details.approvalPath,
|
|
1031
|
+
"--via",
|
|
1032
|
+
"telegram",
|
|
1033
|
+
"--chat-id",
|
|
1034
|
+
details.chatId
|
|
1035
|
+
];
|
|
1036
|
+
const pollArgs = [
|
|
1037
|
+
"npx",
|
|
1038
|
+
"@denial-web/clawguard",
|
|
1039
|
+
"approvals",
|
|
1040
|
+
"poll-telegram",
|
|
1041
|
+
details.approvalPath,
|
|
1042
|
+
"--decisions",
|
|
1043
|
+
details.decisionsPath
|
|
1044
|
+
];
|
|
1045
|
+
const applyArgs = [
|
|
1046
|
+
"npx",
|
|
1047
|
+
"@denial-web/clawguard",
|
|
1048
|
+
"approvals",
|
|
1049
|
+
"apply",
|
|
1050
|
+
details.approvalPath,
|
|
1051
|
+
"--id",
|
|
1052
|
+
"<approval-id>",
|
|
1053
|
+
"--decisions",
|
|
1054
|
+
details.decisionsPath
|
|
1055
|
+
];
|
|
1056
|
+
|
|
1057
|
+
return {
|
|
1058
|
+
guardedInstall: installArgs.map(shellQuote).join(" "),
|
|
1059
|
+
watchTelegram: `TELEGRAM_BOT_TOKEN=<token> ${watchArgs.map(shellQuote).join(" ")}`,
|
|
1060
|
+
pollTelegram: `TELEGRAM_BOT_TOKEN=<token> ${pollArgs.map(shellQuote).join(" ")}`,
|
|
1061
|
+
applyDecision: applyArgs.map(shellQuote).join(" ")
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
665
1065
|
async function readLatestApprovalDecision(decisionsPath, approvalId) {
|
|
666
1066
|
let decisions;
|
|
667
1067
|
|
|
@@ -941,6 +1341,48 @@ function printApprovalApplyResult(result) {
|
|
|
941
1341
|
console.log(`Reason: ${result.reason}`);
|
|
942
1342
|
}
|
|
943
1343
|
|
|
1344
|
+
function printApprovalDoctorResult(result) {
|
|
1345
|
+
console.log("ClawGuard approvals doctor");
|
|
1346
|
+
console.log(`Framework: ${displayFramework(result.framework)}`);
|
|
1347
|
+
console.log(`Ready: ${result.ok ? "yes" : "no"}`);
|
|
1348
|
+
console.log("\nChecks:");
|
|
1349
|
+
for (const check of result.checks) {
|
|
1350
|
+
console.log(`- [${check.status.toUpperCase()}] ${check.message}`);
|
|
1351
|
+
if (check.detail) {
|
|
1352
|
+
console.log(` ${check.detail}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
console.log("\nSuggested commands:");
|
|
1356
|
+
console.log(`1. ${result.commands.guardedInstall}`);
|
|
1357
|
+
console.log(`2. ${result.commands.watchTelegram}`);
|
|
1358
|
+
console.log(`3. ${result.commands.pollTelegram}`);
|
|
1359
|
+
console.log(`4. ${result.commands.applyDecision}`);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function printApprovalDemoFlowResult(result) {
|
|
1363
|
+
console.log("ClawGuard approvals demo-flow");
|
|
1364
|
+
console.log(`Framework: ${displayFramework(result.framework)}`);
|
|
1365
|
+
console.log(`Policy: ${result.policy}`);
|
|
1366
|
+
console.log(`Ready: ${result.ok ? "yes" : "no"}`);
|
|
1367
|
+
console.log(`Workspace: ${result.workspace}${result.cleanedUp ? " (cleaned up)" : ""}`);
|
|
1368
|
+
console.log(`Approval id: ${result.approval.id}`);
|
|
1369
|
+
console.log(`Scan: ${formatDecision(result.scan.decision)} / ${result.scan.risk.level.toUpperCase()} (${result.scan.risk.score}/100)`);
|
|
1370
|
+
console.log(`Decision: ${formatDecision(result.decision.decision)}`);
|
|
1371
|
+
console.log(`Installed: ${result.apply.installed ? "yes" : "no"}`);
|
|
1372
|
+
|
|
1373
|
+
console.log("\nSteps:");
|
|
1374
|
+
for (const step of result.steps) {
|
|
1375
|
+
console.log(`- [${step.status.toUpperCase()}] ${step.name}: ${step.detail}`);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (!result.cleanedUp) {
|
|
1379
|
+
console.log("\nArtifacts:");
|
|
1380
|
+
console.log(`Approval queue: ${result.paths.approvalPath}`);
|
|
1381
|
+
console.log(`Decision log: ${result.paths.decisionsPath}`);
|
|
1382
|
+
console.log(`Installed skill: ${result.paths.installedSkill}`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
944
1386
|
async function readApprovalRequest(approvalPath, id) {
|
|
945
1387
|
const resolvedPath = path.resolve(approvalPath);
|
|
946
1388
|
const approvals = await readApprovalRequests(resolvedPath);
|
|
@@ -1374,6 +1816,14 @@ function commandLabel(commandName) {
|
|
|
1374
1816
|
return "Approval apply";
|
|
1375
1817
|
}
|
|
1376
1818
|
|
|
1819
|
+
if (commandName === "approvals-doctor") {
|
|
1820
|
+
return "Approvals doctor";
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
if (commandName === "approvals-demo-flow") {
|
|
1824
|
+
return "Approvals demo flow";
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1377
1827
|
if (commandName === "gate") {
|
|
1378
1828
|
return "Gate";
|
|
1379
1829
|
}
|
|
@@ -2094,6 +2544,146 @@ function parseApprovalApplyOptions(values) {
|
|
|
2094
2544
|
return options;
|
|
2095
2545
|
}
|
|
2096
2546
|
|
|
2547
|
+
function parseApprovalDoctorOptions(values) {
|
|
2548
|
+
const options = {
|
|
2549
|
+
approvalPath: ".clawguard/approvals.jsonl",
|
|
2550
|
+
decisionsPath: ".clawguard/decisions.jsonl",
|
|
2551
|
+
installDir: ".agents/skills",
|
|
2552
|
+
target: "./candidate-skill",
|
|
2553
|
+
framework: "openclaw",
|
|
2554
|
+
chatId: undefined,
|
|
2555
|
+
botToken: undefined,
|
|
2556
|
+
telegramApiBase: undefined,
|
|
2557
|
+
checkTelegram: false,
|
|
2558
|
+
json: false
|
|
2559
|
+
};
|
|
2560
|
+
|
|
2561
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
2562
|
+
const value = values[index];
|
|
2563
|
+
|
|
2564
|
+
if (value === "--json") {
|
|
2565
|
+
options.json = true;
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
if (value === "--approval-out") {
|
|
2570
|
+
options.approvalPath = requireNextValue(values, index, "--approval-out");
|
|
2571
|
+
index += 1;
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
if (value === "--decisions") {
|
|
2576
|
+
options.decisionsPath = requireNextValue(values, index, "--decisions");
|
|
2577
|
+
index += 1;
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
if (value === "--to") {
|
|
2582
|
+
options.installDir = requireNextValue(values, index, "--to");
|
|
2583
|
+
index += 1;
|
|
2584
|
+
continue;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
if (value === "--target") {
|
|
2588
|
+
options.target = requireNextValue(values, index, "--target");
|
|
2589
|
+
index += 1;
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
if (value === "--framework") {
|
|
2594
|
+
options.framework = requireNextValue(values, index, "--framework");
|
|
2595
|
+
index += 1;
|
|
2596
|
+
continue;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
if (value === "--chat-id") {
|
|
2600
|
+
options.chatId = requireNextValue(values, index, "--chat-id");
|
|
2601
|
+
index += 1;
|
|
2602
|
+
continue;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
if (value === "--bot-token") {
|
|
2606
|
+
options.botToken = requireNextValue(values, index, "--bot-token");
|
|
2607
|
+
index += 1;
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
if (value === "--telegram-api-base") {
|
|
2612
|
+
options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
|
|
2613
|
+
index += 1;
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
if (value === "--check-telegram") {
|
|
2618
|
+
options.checkTelegram = true;
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
if (value.startsWith("--")) {
|
|
2623
|
+
throw new Error(`Unknown option: ${value}`);
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
throw new Error(`Unexpected argument for approvals doctor: ${value}`);
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
if (!["openclaw", "hermes"].includes(options.framework)) {
|
|
2630
|
+
throw new Error("Invalid --framework value. Use one of: openclaw, hermes");
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
return options;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function parseApprovalDemoFlowOptions(values) {
|
|
2637
|
+
const options = {
|
|
2638
|
+
framework: "openclaw",
|
|
2639
|
+
policy: "governed",
|
|
2640
|
+
keep: false,
|
|
2641
|
+
json: false
|
|
2642
|
+
};
|
|
2643
|
+
|
|
2644
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
2645
|
+
const value = values[index];
|
|
2646
|
+
|
|
2647
|
+
if (value === "--json") {
|
|
2648
|
+
options.json = true;
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
if (value === "--keep") {
|
|
2653
|
+
options.keep = true;
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
if (value === "--framework") {
|
|
2658
|
+
options.framework = requireNextValue(values, index, "--framework");
|
|
2659
|
+
index += 1;
|
|
2660
|
+
continue;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
if (value === "--policy") {
|
|
2664
|
+
options.policy = requireNextValue(values, index, "--policy");
|
|
2665
|
+
index += 1;
|
|
2666
|
+
continue;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
if (value.startsWith("--")) {
|
|
2670
|
+
throw new Error(`Unknown option: ${value}`);
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
throw new Error(`Unexpected argument for approvals demo-flow: ${value}`);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
if (!["openclaw", "hermes"].includes(options.framework)) {
|
|
2677
|
+
throw new Error("Invalid --framework value. Use one of: openclaw, hermes");
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
if (!policyPresets.includes(options.policy)) {
|
|
2681
|
+
throw new Error(`Invalid --policy value. Use one of: ${policyPresets.join(", ")}`);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
return options;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2097
2687
|
async function writeReportFile(outputPath, content) {
|
|
2098
2688
|
const resolvedPath = path.resolve(outputPath);
|
|
2099
2689
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|