@denial-web/clawguard 0.1.12 → 0.1.14
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 +10 -0
- package/package.json +1 -1
- package/src/cli.js +253 -1
- package/web/app.js +49 -0
- package/web/index.html +22 -0
- package/web/styles.css +47 -2
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
|
+
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
|
+
|
|
71
79
|
Check the approval setup and print the exact command flow:
|
|
72
80
|
|
|
73
81
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -78,6 +78,16 @@ 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
|
+
|
|
81
91
|
### Approval Doctor
|
|
82
92
|
|
|
83
93
|
Before wiring a real agent into the approval loop, users can run:
|
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";
|
|
@@ -42,7 +43,8 @@ if (![
|
|
|
42
43
|
"approvals-decide",
|
|
43
44
|
"approvals-poll-telegram",
|
|
44
45
|
"approvals-apply",
|
|
45
|
-
"approvals-doctor"
|
|
46
|
+
"approvals-doctor",
|
|
47
|
+
"approvals-demo-flow"
|
|
46
48
|
].includes(command)) {
|
|
47
49
|
console.error(`Unknown command: ${command}`);
|
|
48
50
|
printHelp();
|
|
@@ -118,6 +120,17 @@ try {
|
|
|
118
120
|
process.exit(result.ok ? 0 : 1);
|
|
119
121
|
}
|
|
120
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
|
+
|
|
121
134
|
const cliOptions = parseOptions(optionValues);
|
|
122
135
|
cliOptions.framework = framework;
|
|
123
136
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -184,6 +197,7 @@ Usage:
|
|
|
184
197
|
clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
|
|
185
198
|
clawguard approvals apply <approvals.jsonl> --id <id> --decisions <decisions.jsonl>
|
|
186
199
|
clawguard approvals doctor [--chat-id <id>]
|
|
200
|
+
clawguard approvals demo-flow [--keep]
|
|
187
201
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
188
202
|
npm run scan -- <path>
|
|
189
203
|
|
|
@@ -229,6 +243,8 @@ Options:
|
|
|
229
243
|
Read Telegram updates from a JSON file for tests or offline replay.
|
|
230
244
|
--check-telegram In approvals doctor, call Telegram getMe to verify the bot token.
|
|
231
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.
|
|
232
248
|
|
|
233
249
|
Gate exit codes:
|
|
234
250
|
0 = allow
|
|
@@ -248,6 +264,7 @@ Examples:
|
|
|
248
264
|
npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
|
|
249
265
|
npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <id> --decisions ./.clawguard/decisions.jsonl
|
|
250
266
|
npx @denial-web/clawguard approvals doctor --chat-id 123456789
|
|
267
|
+
npx @denial-web/clawguard approvals demo-flow --keep
|
|
251
268
|
npm run scan -- examples/risky-skill
|
|
252
269
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
253
270
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -379,6 +396,14 @@ function parseCommand(values) {
|
|
|
379
396
|
};
|
|
380
397
|
}
|
|
381
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
|
+
|
|
382
407
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
383
408
|
const nestedCommand = values[1];
|
|
384
409
|
|
|
@@ -752,6 +777,154 @@ async function runApprovalDoctor(options) {
|
|
|
752
777
|
};
|
|
753
778
|
}
|
|
754
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
|
+
|
|
755
928
|
function checkNodeVersion() {
|
|
756
929
|
const major = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
757
930
|
return {
|
|
@@ -1186,6 +1359,30 @@ function printApprovalDoctorResult(result) {
|
|
|
1186
1359
|
console.log(`4. ${result.commands.applyDecision}`);
|
|
1187
1360
|
}
|
|
1188
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
|
+
|
|
1189
1386
|
async function readApprovalRequest(approvalPath, id) {
|
|
1190
1387
|
const resolvedPath = path.resolve(approvalPath);
|
|
1191
1388
|
const approvals = await readApprovalRequests(resolvedPath);
|
|
@@ -1623,6 +1820,10 @@ function commandLabel(commandName) {
|
|
|
1623
1820
|
return "Approvals doctor";
|
|
1624
1821
|
}
|
|
1625
1822
|
|
|
1823
|
+
if (commandName === "approvals-demo-flow") {
|
|
1824
|
+
return "Approvals demo flow";
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1626
1827
|
if (commandName === "gate") {
|
|
1627
1828
|
return "Gate";
|
|
1628
1829
|
}
|
|
@@ -2432,6 +2633,57 @@ function parseApprovalDoctorOptions(values) {
|
|
|
2432
2633
|
return options;
|
|
2433
2634
|
}
|
|
2434
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
|
+
|
|
2435
2687
|
async function writeReportFile(outputPath, content) {
|
|
2436
2688
|
const resolvedPath = path.resolve(outputPath);
|
|
2437
2689
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
package/web/app.js
CHANGED
|
@@ -40,6 +40,10 @@ const elements = {
|
|
|
40
40
|
installVerdict: document.querySelector("#install-verdict"),
|
|
41
41
|
installMessage: document.querySelector("#install-message"),
|
|
42
42
|
installCommand: document.querySelector("#install-command"),
|
|
43
|
+
approvalTitle: document.querySelector("#approval-title"),
|
|
44
|
+
approvalSummary: document.querySelector("#approval-summary"),
|
|
45
|
+
approvalCommand: document.querySelector("#approval-command"),
|
|
46
|
+
demoCommand: document.querySelector("#demo-command"),
|
|
43
47
|
actions: document.querySelector("#actions"),
|
|
44
48
|
critical: document.querySelector("#critical-count"),
|
|
45
49
|
high: document.querySelector("#high-count"),
|
|
@@ -239,6 +243,7 @@ function renderResult(result) {
|
|
|
239
243
|
elements.downloadHtml.disabled = false;
|
|
240
244
|
|
|
241
245
|
renderInstallGate(result);
|
|
246
|
+
renderApprovalFlow(result);
|
|
242
247
|
renderFindings(scan.findings ?? []);
|
|
243
248
|
}
|
|
244
249
|
|
|
@@ -268,6 +273,50 @@ function renderInstallGate(result) {
|
|
|
268
273
|
elements.installMessage.textContent = "ClawGuard would require review, sandboxing, or approval before copying files.";
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
function renderApprovalFlow(result) {
|
|
277
|
+
const scan = result.scan;
|
|
278
|
+
const policy = scan.policy ?? {};
|
|
279
|
+
const decision = policy.decision ?? "allow";
|
|
280
|
+
const target = installTargetFor(result);
|
|
281
|
+
const installName = safeInstallName(result.displayTarget ?? "skill");
|
|
282
|
+
const preset = policy.preset ?? elements.policy.value;
|
|
283
|
+
const framework = "openclaw";
|
|
284
|
+
|
|
285
|
+
elements.demoCommand.textContent = "npx @denial-web/clawguard approvals demo-flow --keep";
|
|
286
|
+
elements.approvalCommand.textContent = [
|
|
287
|
+
"npx",
|
|
288
|
+
"@denial-web/clawguard",
|
|
289
|
+
framework,
|
|
290
|
+
"install",
|
|
291
|
+
target,
|
|
292
|
+
"--to",
|
|
293
|
+
"./.agents/skills",
|
|
294
|
+
"--name",
|
|
295
|
+
installName,
|
|
296
|
+
"--policy",
|
|
297
|
+
preset,
|
|
298
|
+
"--approval-out",
|
|
299
|
+
"./.clawguard/approvals.jsonl",
|
|
300
|
+
"--approval-mode",
|
|
301
|
+
"always"
|
|
302
|
+
].join(" ");
|
|
303
|
+
|
|
304
|
+
if (decision === "allow") {
|
|
305
|
+
elements.approvalTitle.textContent = "Approval can still be required";
|
|
306
|
+
elements.approvalSummary.textContent = "Even a clean scan can pause before trust when you use approval-mode always. That gives the owner final control over autonomous skill installs.";
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (decision === "block") {
|
|
311
|
+
elements.approvalTitle.textContent = "Blocked before trust";
|
|
312
|
+
elements.approvalSummary.textContent = "A blocked result should not be copied into a trusted skill folder. Send the report to the owner instead of applying the install.";
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
elements.approvalTitle.textContent = "Pause and ask the owner";
|
|
317
|
+
elements.approvalSummary.textContent = "Warn, review, sandbox, and dual-approval decisions create a pending approval request before any files are copied into the trusted skill folder.";
|
|
318
|
+
}
|
|
319
|
+
|
|
271
320
|
function renderFindings(findings) {
|
|
272
321
|
if (findings.length === 0) {
|
|
273
322
|
elements.findings.className = "findings empty-state";
|
package/web/index.html
CHANGED
|
@@ -86,6 +86,28 @@
|
|
|
86
86
|
</div>
|
|
87
87
|
</section>
|
|
88
88
|
|
|
89
|
+
<section class="approval-panel" aria-label="Approval loop demo">
|
|
90
|
+
<div class="approval-copy">
|
|
91
|
+
<p class="eyebrow">Approval Loop Demo</p>
|
|
92
|
+
<h2 id="approval-title">Prove the full guarded install loop</h2>
|
|
93
|
+
<p id="approval-summary">Create a harmless temporary skill, scan it, write an approval request, record an owner approval, and install only after the decision is applied.</p>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="approval-flow" aria-label="Approval flow">
|
|
96
|
+
<span>install hook</span>
|
|
97
|
+
<span>policy gate</span>
|
|
98
|
+
<span>owner approval</span>
|
|
99
|
+
<span>apply</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="command-stack">
|
|
102
|
+
<div class="command-box">
|
|
103
|
+
<code id="demo-command">npx @denial-web/clawguard approvals demo-flow --keep</code>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="command-box">
|
|
106
|
+
<code id="approval-command">npx @denial-web/clawguard openclaw install ./skill --to ./.agents/skills --approval-out ./.clawguard/approvals.jsonl --approval-mode always</code>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</section>
|
|
110
|
+
|
|
89
111
|
<section class="metrics" aria-label="Finding summary">
|
|
90
112
|
<div><span>Critical</span><strong id="critical-count">0</strong></div>
|
|
91
113
|
<div><span>High</span><strong id="high-count">0</strong></div>
|
package/web/styles.css
CHANGED
|
@@ -264,7 +264,7 @@ input[type="file"] {
|
|
|
264
264
|
align-items: stretch;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
.score-ring, .decision, .install-panel, .metrics div, .metadata-grid div, .finding-card, .empty-state {
|
|
267
|
+
.score-ring, .decision, .install-panel, .approval-panel, .metrics div, .metadata-grid div, .finding-card, .empty-state {
|
|
268
268
|
border: 1px solid var(--line);
|
|
269
269
|
border-radius: 8px;
|
|
270
270
|
background: var(--panel);
|
|
@@ -339,6 +339,51 @@ input[type="file"] {
|
|
|
339
339
|
overflow-wrap: anywhere;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
.approval-panel {
|
|
343
|
+
display: grid;
|
|
344
|
+
grid-template-columns: minmax(220px, 0.9fr) minmax(160px, 0.65fr) minmax(0, 1.25fr);
|
|
345
|
+
gap: 14px;
|
|
346
|
+
align-items: stretch;
|
|
347
|
+
padding: 16px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.approval-copy h2 {
|
|
351
|
+
margin-top: 5px;
|
|
352
|
+
font-size: 22px;
|
|
353
|
+
text-transform: uppercase;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#approval-summary {
|
|
357
|
+
margin-top: 6px;
|
|
358
|
+
color: var(--muted);
|
|
359
|
+
line-height: 1.4;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.approval-flow {
|
|
363
|
+
display: grid;
|
|
364
|
+
gap: 8px;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.approval-flow span {
|
|
368
|
+
min-height: 34px;
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: center;
|
|
371
|
+
border: 1px solid var(--line);
|
|
372
|
+
border-radius: 8px;
|
|
373
|
+
background: #edf1ea;
|
|
374
|
+
color: var(--ink);
|
|
375
|
+
padding: 7px 10px;
|
|
376
|
+
font-size: 12px;
|
|
377
|
+
font-weight: 900;
|
|
378
|
+
text-transform: uppercase;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.command-stack {
|
|
382
|
+
min-width: 0;
|
|
383
|
+
display: grid;
|
|
384
|
+
gap: 10px;
|
|
385
|
+
}
|
|
386
|
+
|
|
342
387
|
.action-tags {
|
|
343
388
|
display: flex;
|
|
344
389
|
flex-wrap: wrap;
|
|
@@ -459,7 +504,7 @@ input[type="file"] {
|
|
|
459
504
|
}
|
|
460
505
|
|
|
461
506
|
@media (max-width: 900px) {
|
|
462
|
-
.workbench, .score-panel, .install-panel {
|
|
507
|
+
.workbench, .score-panel, .install-panel, .approval-panel {
|
|
463
508
|
grid-template-columns: 1fr;
|
|
464
509
|
}
|
|
465
510
|
|