@denial-web/clawguard 0.1.2 → 0.1.4
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 +9 -0
- package/docs/INTEGRATION_SPEC.md +40 -0
- package/package.json +1 -1
- package/scripts/capture-demo.js +2 -1
- package/src/cli.js +214 -7
- package/src/config.js +3 -1
- package/web/app.js +51 -0
- package/web/index.html +13 -2
- package/web/styles.css +39 -2
package/README.md
CHANGED
|
@@ -59,6 +59,15 @@ npx @denial-web/clawguard install ./path/to/skill --to ./.agents/skills --policy
|
|
|
59
59
|
|
|
60
60
|
Install mode never executes scanned files or installs dependencies. It refuses warn/review/sandbox/block decisions before copying files.
|
|
61
61
|
|
|
62
|
+
For agent systems that search and install skills automatically, keep discovery native and gate only the install step:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx @denial-web/clawguard openclaw install ./candidate-skill --to ./.agents/skills --approval-out ./.clawguard/approvals.jsonl
|
|
66
|
+
npx @denial-web/clawguard hermes install ./candidate-skill --to ~/.hermes/skills --approval-out ./.clawguard/approvals.jsonl
|
|
67
|
+
```
|
|
68
|
+
|
|
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
|
+
|
|
62
71
|
When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
|
|
63
72
|
|
|
64
73
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -13,6 +13,32 @@ This spec defines how ClawGuard should work with OpenClaw, ClawHub, GitHub, web
|
|
|
13
13
|
|
|
14
14
|
## OpenClaw Integration
|
|
15
15
|
|
|
16
|
+
### Guarded Install With Owner Approval
|
|
17
|
+
|
|
18
|
+
Current wrapper pattern:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
clawguard openclaw install ./candidate-skill \
|
|
22
|
+
--to ./.agents/skills \
|
|
23
|
+
--approval-out ./.clawguard/approvals.jsonl
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This does not block OpenClaw or ClawHub discovery. Search and candidate selection can stay native. ClawGuard sits between the downloaded candidate and the trusted skill folder:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
native search/discovery
|
|
30
|
+
↓
|
|
31
|
+
candidate skill bundle
|
|
32
|
+
↓
|
|
33
|
+
clawguard openclaw install
|
|
34
|
+
↓
|
|
35
|
+
allow / approval request / block
|
|
36
|
+
↓
|
|
37
|
+
trusted skill folder
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If `--approval-out` is set, non-allow decisions create a pending approval JSON payload instead of copying files. With `--approval-mode always`, even allow decisions pause for explicit owner approval. A messaging adapter can forward the `message` field to WhatsApp, Telegram, Slack, Discord, or another owner channel.
|
|
41
|
+
|
|
16
42
|
### Skill Folder Scan
|
|
17
43
|
|
|
18
44
|
Command:
|
|
@@ -107,6 +133,20 @@ Current implementation:
|
|
|
107
133
|
- Reports missing lockfile, missing origin metadata, version drift, source drift, invalid metadata, and unusual source URLs.
|
|
108
134
|
- Adds a `clawhub` summary to JSON and HTML reports.
|
|
109
135
|
|
|
136
|
+
## Hermes Agent Integration
|
|
137
|
+
|
|
138
|
+
Current wrapper pattern:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
clawguard hermes install ./candidate-skill \
|
|
142
|
+
--to ~/.hermes/skills \
|
|
143
|
+
--approval-out ./.clawguard/approvals.jsonl
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The first integration target is the same as OpenClaw: do not interfere with search or discovery. Scan the candidate before it is copied into a trusted Hermes skill directory, and emit an approval request when policy says the owner should decide.
|
|
147
|
+
|
|
148
|
+
ClawGuard is independent and not affiliated with Hermes Agent or Nous Research.
|
|
149
|
+
|
|
110
150
|
### Metadata Comparison
|
|
111
151
|
|
|
112
152
|
ClawGuard should compare:
|
package/package.json
CHANGED
package/scripts/capture-demo.js
CHANGED
|
@@ -89,7 +89,8 @@ async function main() {
|
|
|
89
89
|
await pause(250);
|
|
90
90
|
await dependencyRisk.click();
|
|
91
91
|
await page.getByRole("heading", { name: "Dependency Risk" }).waitFor();
|
|
92
|
-
await page.
|
|
92
|
+
await page.getByRole("heading", { name: "Block", exact: true }).waitFor();
|
|
93
|
+
await page.getByRole("heading", { name: "Install blocked" }).waitFor();
|
|
93
94
|
await pause(700);
|
|
94
95
|
|
|
95
96
|
await page.screenshot({
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { promises as fs } from "node:fs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { loadConfig, mergeConfig, parseSize } from "./config.js";
|
|
6
7
|
import { policyShouldFail } from "./policy.js";
|
|
@@ -25,7 +26,8 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
|
25
26
|
process.exit(0);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
+
const commandContext = parseCommand(args);
|
|
30
|
+
const { command, framework, optionValues } = commandContext;
|
|
29
31
|
|
|
30
32
|
if (!["scan", "scan-workspace", "gate", "install"].includes(command)) {
|
|
31
33
|
console.error(`Unknown command: ${command}`);
|
|
@@ -34,9 +36,11 @@ if (!["scan", "scan-workspace", "gate", "install"].includes(command)) {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
try {
|
|
37
|
-
const cliOptions = parseOptions(
|
|
39
|
+
const cliOptions = parseOptions(optionValues);
|
|
40
|
+
cliOptions.framework = framework;
|
|
38
41
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
39
42
|
const options = mergeConfig(loadedConfig.config, cliOptions);
|
|
43
|
+
options.framework = framework;
|
|
40
44
|
const result = await scanTarget(options.target, {
|
|
41
45
|
maxFileSizeBytes: options.maxFileSizeBytes,
|
|
42
46
|
maxFindingsPerRulePerFile: options.maxFindingsPerRulePerFile,
|
|
@@ -54,6 +58,8 @@ try {
|
|
|
54
58
|
await writeReportFile(options.htmlPath, createHtmlReport(result));
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
let exitCode;
|
|
62
|
+
|
|
57
63
|
if (command === "install") {
|
|
58
64
|
const install = await handleInstall(result, options);
|
|
59
65
|
if (options.json) {
|
|
@@ -61,6 +67,7 @@ try {
|
|
|
61
67
|
} else {
|
|
62
68
|
printInstallResult(result, install);
|
|
63
69
|
}
|
|
70
|
+
exitCode = installExitCode(result.policy.decision, install);
|
|
64
71
|
} else if (command === "gate") {
|
|
65
72
|
if (options.json) {
|
|
66
73
|
console.log(JSON.stringify(createGateResult(result), null, 2));
|
|
@@ -73,7 +80,7 @@ try {
|
|
|
73
80
|
printHumanResult(result, options);
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
process.exit(
|
|
83
|
+
process.exit(exitCode ?? (command === "gate" ? gateExitCode(result.policy.decision) : shouldFail(result, options) ? 2 : 0));
|
|
77
84
|
} catch (error) {
|
|
78
85
|
console.error(`${commandLabel(command)} failed: ${error.message}`);
|
|
79
86
|
process.exit(1);
|
|
@@ -86,6 +93,8 @@ Usage:
|
|
|
86
93
|
clawguard scan <path> [--json] [--policy <preset>] [--fail-on <level>]
|
|
87
94
|
clawguard gate <path> [--json] [--policy <preset>]
|
|
88
95
|
clawguard install <path> --to <dir> [--policy <preset>] [--dry-run]
|
|
96
|
+
clawguard openclaw install <path> --to <dir> [--approval-out <path>]
|
|
97
|
+
clawguard hermes install <path> --to <dir> [--approval-out <path>]
|
|
89
98
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
90
99
|
npm run scan -- <path>
|
|
91
100
|
|
|
@@ -107,6 +116,9 @@ Options:
|
|
|
107
116
|
--to <dir> Install destination parent directory for install mode.
|
|
108
117
|
--name <name> Install folder/file name. Defaults to the source basename.
|
|
109
118
|
--dry-run Run install gate and show the destination without copying files.
|
|
119
|
+
--approval-out <path> Write a pending approval JSON request before copying.
|
|
120
|
+
Use .jsonl to append JSON lines for bot/daemon integrations.
|
|
121
|
+
--approval-mode <mode> Approval mode: non-allow, always. Default: non-allow.
|
|
110
122
|
|
|
111
123
|
Gate exit codes:
|
|
112
124
|
0 = allow
|
|
@@ -117,6 +129,8 @@ Examples:
|
|
|
117
129
|
npx @denial-web/clawguard gate ./skills/my-skill
|
|
118
130
|
npx @denial-web/clawguard gate ./skills/my-skill --policy governed
|
|
119
131
|
npx @denial-web/clawguard install ./skills/my-skill --to ./.agents/skills --policy governed
|
|
132
|
+
npx @denial-web/clawguard openclaw install ./skills/my-skill --to ./.agents/skills --approval-out ./.clawguard/approvals.jsonl
|
|
133
|
+
npx @denial-web/clawguard hermes install ./skills/my-skill --to ~/.hermes/skills --approval-out ./.clawguard/approvals.jsonl
|
|
120
134
|
npm run scan -- examples/risky-skill
|
|
121
135
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
122
136
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -197,6 +211,42 @@ function printHumanResult(result, options) {
|
|
|
197
211
|
}
|
|
198
212
|
}
|
|
199
213
|
|
|
214
|
+
function parseCommand(values) {
|
|
215
|
+
const rawCommand = values[0];
|
|
216
|
+
|
|
217
|
+
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
218
|
+
const nestedCommand = values[1];
|
|
219
|
+
|
|
220
|
+
if (!nestedCommand) {
|
|
221
|
+
return {
|
|
222
|
+
command: "",
|
|
223
|
+
framework: rawCommand,
|
|
224
|
+
optionValues: []
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!["gate", "install"].includes(nestedCommand)) {
|
|
229
|
+
return {
|
|
230
|
+
command: `${rawCommand} ${nestedCommand}`,
|
|
231
|
+
framework: rawCommand,
|
|
232
|
+
optionValues: values.slice(2)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
command: nestedCommand,
|
|
238
|
+
framework: rawCommand,
|
|
239
|
+
optionValues: values.slice(2)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
command: rawCommand,
|
|
245
|
+
framework: undefined,
|
|
246
|
+
optionValues: values.slice(1)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
200
250
|
function printGateResult(result, options) {
|
|
201
251
|
const decision = result.policy.decision;
|
|
202
252
|
console.log(`ClawGuard gate: ${result.target}`);
|
|
@@ -243,10 +293,18 @@ async function handleInstall(result, options) {
|
|
|
243
293
|
const install = {
|
|
244
294
|
destination,
|
|
245
295
|
dryRun: options.dryRun,
|
|
296
|
+
framework: options.framework,
|
|
246
297
|
installed: false,
|
|
247
|
-
skipped: decision !== "allow"
|
|
298
|
+
skipped: decision !== "allow",
|
|
299
|
+
approvalRequest: null
|
|
248
300
|
};
|
|
249
301
|
|
|
302
|
+
if (shouldCreateApprovalRequest(decision, options)) {
|
|
303
|
+
install.approvalRequest = await writeApprovalRequest(result, install, options);
|
|
304
|
+
install.skipped = true;
|
|
305
|
+
return install;
|
|
306
|
+
}
|
|
307
|
+
|
|
250
308
|
if (decision !== "allow") {
|
|
251
309
|
return install;
|
|
252
310
|
}
|
|
@@ -277,10 +335,13 @@ async function handleInstall(result, options) {
|
|
|
277
335
|
function printInstallResult(result, install) {
|
|
278
336
|
const decision = result.policy.decision;
|
|
279
337
|
console.log(`ClawGuard install: ${result.target}`);
|
|
338
|
+
if (install.framework) {
|
|
339
|
+
console.log(`Framework: ${displayFramework(install.framework)}`);
|
|
340
|
+
}
|
|
280
341
|
console.log(`Decision: ${formatDecision(decision)}`);
|
|
281
342
|
console.log(`Risk: ${result.level.toUpperCase()} (${result.score}/100)`);
|
|
282
343
|
console.log(`Policy: ${result.policy.preset}`);
|
|
283
|
-
console.log(`Exit code: ${
|
|
344
|
+
console.log(`Exit code: ${installExitCode(decision, install)}`);
|
|
284
345
|
console.log(`Destination: ${install.destination ?? "not selected"}`);
|
|
285
346
|
console.log(`Installed: ${install.installed ? "yes" : "no"}`);
|
|
286
347
|
|
|
@@ -292,7 +353,11 @@ function printInstallResult(result, install) {
|
|
|
292
353
|
console.log(`Required actions: ${result.policy.requiredActions.join(", ")}`);
|
|
293
354
|
}
|
|
294
355
|
|
|
295
|
-
if (
|
|
356
|
+
if (install.approvalRequest) {
|
|
357
|
+
console.log(`Approval request: ${install.approvalRequest.path}`);
|
|
358
|
+
console.log(`Approval id: ${install.approvalRequest.id}`);
|
|
359
|
+
console.log("\nInstall result: pending user approval before copying files.");
|
|
360
|
+
} else if (decision === "allow" && install.installed) {
|
|
296
361
|
console.log("\nInstall result: copied after passing the selected policy.");
|
|
297
362
|
} else if (decision === "allow" && install.dryRun) {
|
|
298
363
|
console.log("\nInstall result: dry run passed; no files were copied.");
|
|
@@ -308,10 +373,13 @@ function printInstallResult(result, install) {
|
|
|
308
373
|
function createInstallResult(result, install) {
|
|
309
374
|
return {
|
|
310
375
|
...createGateResult(result),
|
|
376
|
+
exitCode: installExitCode(result.policy.decision, install),
|
|
377
|
+
framework: install.framework,
|
|
311
378
|
destination: install.destination,
|
|
312
379
|
installed: install.installed,
|
|
313
380
|
dryRun: install.dryRun,
|
|
314
|
-
skipped: install.skipped
|
|
381
|
+
skipped: install.skipped,
|
|
382
|
+
approvalRequest: install.approvalRequest
|
|
315
383
|
};
|
|
316
384
|
}
|
|
317
385
|
|
|
@@ -350,6 +418,106 @@ function resolveInstallDestination(sourcePath, options) {
|
|
|
350
418
|
return path.resolve(options.installDir, installName);
|
|
351
419
|
}
|
|
352
420
|
|
|
421
|
+
function shouldCreateApprovalRequest(decision, options) {
|
|
422
|
+
if (!options.approvalOut) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (options.approvalMode === "always") {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return decision !== "allow";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function writeApprovalRequest(result, install, options) {
|
|
434
|
+
const request = createApprovalRequest(result, install, options);
|
|
435
|
+
const outputPath = path.resolve(options.approvalOut);
|
|
436
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
437
|
+
|
|
438
|
+
if (outputPath.endsWith(".jsonl")) {
|
|
439
|
+
await fs.appendFile(outputPath, `${JSON.stringify(request)}\n`);
|
|
440
|
+
} else {
|
|
441
|
+
await fs.writeFile(outputPath, `${JSON.stringify(request, null, 2)}\n`, { flag: "wx" });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
id: request.id,
|
|
446
|
+
path: outputPath,
|
|
447
|
+
status: request.status,
|
|
448
|
+
message: request.message
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function createApprovalRequest(result, install, options) {
|
|
453
|
+
const id = randomUUID();
|
|
454
|
+
const decision = result.policy.decision;
|
|
455
|
+
const framework = options.framework ?? "generic";
|
|
456
|
+
const target = path.resolve(options.target);
|
|
457
|
+
const topFindings = result.findings.slice(0, 5).map((finding) => ({
|
|
458
|
+
ruleId: finding.ruleId,
|
|
459
|
+
severity: finding.severity,
|
|
460
|
+
title: finding.title,
|
|
461
|
+
file: finding.file,
|
|
462
|
+
line: finding.line,
|
|
463
|
+
recommendation: finding.recommendation
|
|
464
|
+
}));
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
schemaVersion: "clawguard.approval.v1",
|
|
468
|
+
id,
|
|
469
|
+
status: "pending",
|
|
470
|
+
createdAt: new Date().toISOString(),
|
|
471
|
+
framework,
|
|
472
|
+
target,
|
|
473
|
+
destination: install.destination,
|
|
474
|
+
decision,
|
|
475
|
+
risk: {
|
|
476
|
+
level: result.level,
|
|
477
|
+
score: result.score
|
|
478
|
+
},
|
|
479
|
+
policy: {
|
|
480
|
+
preset: result.policy.preset,
|
|
481
|
+
reason: result.policy.reason,
|
|
482
|
+
requiredActions: result.policy.requiredActions
|
|
483
|
+
},
|
|
484
|
+
install: {
|
|
485
|
+
dryRun: install.dryRun,
|
|
486
|
+
installed: false,
|
|
487
|
+
skipped: true
|
|
488
|
+
},
|
|
489
|
+
summary: result.summary,
|
|
490
|
+
findings: topFindings,
|
|
491
|
+
message: createApprovalMessage({
|
|
492
|
+
framework,
|
|
493
|
+
target,
|
|
494
|
+
destination: install.destination,
|
|
495
|
+
decision,
|
|
496
|
+
risk: result.level,
|
|
497
|
+
score: result.score,
|
|
498
|
+
requiredActions: result.policy.requiredActions,
|
|
499
|
+
findings: topFindings
|
|
500
|
+
})
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function createApprovalMessage(details) {
|
|
505
|
+
const findingLines = details.findings.length === 0
|
|
506
|
+
? "No findings were reported."
|
|
507
|
+
: details.findings.map((finding) => `- ${finding.severity.toUpperCase()}: ${finding.title}`).join("\n");
|
|
508
|
+
|
|
509
|
+
return [
|
|
510
|
+
`ClawGuard approval needed for ${displayFramework(details.framework)} skill install.`,
|
|
511
|
+
`Decision: ${formatDecision(details.decision)}`,
|
|
512
|
+
`Risk: ${details.risk.toUpperCase()} (${details.score}/100)`,
|
|
513
|
+
`Source: ${details.target}`,
|
|
514
|
+
`Destination: ${details.destination ?? "not selected"}`,
|
|
515
|
+
`Required actions: ${details.requiredActions.length > 0 ? details.requiredActions.join(", ") : "none"}`,
|
|
516
|
+
"Top findings:",
|
|
517
|
+
findingLines
|
|
518
|
+
].join("\n");
|
|
519
|
+
}
|
|
520
|
+
|
|
353
521
|
async function assertInstallableSource(sourcePath) {
|
|
354
522
|
const stats = await fs.lstat(sourcePath);
|
|
355
523
|
|
|
@@ -404,6 +572,18 @@ function commandLabel(commandName) {
|
|
|
404
572
|
return "Scan";
|
|
405
573
|
}
|
|
406
574
|
|
|
575
|
+
function displayFramework(value) {
|
|
576
|
+
if (value === "openclaw") {
|
|
577
|
+
return "OpenClaw";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (value === "hermes") {
|
|
581
|
+
return "Hermes Agent";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return "agent";
|
|
585
|
+
}
|
|
586
|
+
|
|
407
587
|
function formatDecision(decision) {
|
|
408
588
|
return decision.replaceAll("_", " ").toUpperCase();
|
|
409
589
|
}
|
|
@@ -420,6 +600,14 @@ function gateExitCode(decision) {
|
|
|
420
600
|
return 1;
|
|
421
601
|
}
|
|
422
602
|
|
|
603
|
+
function installExitCode(decision, install) {
|
|
604
|
+
if (install.approvalRequest) {
|
|
605
|
+
return 1;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return gateExitCode(decision);
|
|
609
|
+
}
|
|
610
|
+
|
|
423
611
|
function parseOptions(values) {
|
|
424
612
|
const options = {
|
|
425
613
|
json: false,
|
|
@@ -434,6 +622,9 @@ function parseOptions(values) {
|
|
|
434
622
|
installDir: undefined,
|
|
435
623
|
installName: undefined,
|
|
436
624
|
dryRun: false,
|
|
625
|
+
approvalOut: undefined,
|
|
626
|
+
approvalMode: "non-allow",
|
|
627
|
+
framework: undefined,
|
|
437
628
|
target: "."
|
|
438
629
|
};
|
|
439
630
|
const paths = [];
|
|
@@ -527,6 +718,22 @@ function parseOptions(values) {
|
|
|
527
718
|
continue;
|
|
528
719
|
}
|
|
529
720
|
|
|
721
|
+
if (value === "--approval-out") {
|
|
722
|
+
options.approvalOut = requireNextValue(values, index, "--approval-out");
|
|
723
|
+
index += 1;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (value === "--approval-mode") {
|
|
728
|
+
const mode = requireNextValue(values, index, "--approval-mode");
|
|
729
|
+
if (!["non-allow", "always"].includes(mode)) {
|
|
730
|
+
throw new Error("Invalid --approval-mode value. Use one of: non-allow, always");
|
|
731
|
+
}
|
|
732
|
+
options.approvalMode = mode;
|
|
733
|
+
index += 1;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
|
|
530
737
|
if (value.startsWith("--")) {
|
|
531
738
|
throw new Error(`Unknown option: ${value}`);
|
|
532
739
|
}
|
package/src/config.js
CHANGED
|
@@ -64,7 +64,9 @@ export function mergeConfig(config, cliOptions = {}) {
|
|
|
64
64
|
sarifPath: cliOptions.sarifPath,
|
|
65
65
|
installDir: cliOptions.installDir,
|
|
66
66
|
installName: cliOptions.installName,
|
|
67
|
-
dryRun: Boolean(cliOptions.dryRun)
|
|
67
|
+
dryRun: Boolean(cliOptions.dryRun),
|
|
68
|
+
approvalOut: cliOptions.approvalOut,
|
|
69
|
+
approvalMode: cliOptions.approvalMode ?? "non-allow"
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
|
package/web/app.js
CHANGED
|
@@ -37,6 +37,9 @@ const elements = {
|
|
|
37
37
|
level: document.querySelector("#level"),
|
|
38
38
|
decision: document.querySelector("#decision"),
|
|
39
39
|
reason: document.querySelector("#reason"),
|
|
40
|
+
installVerdict: document.querySelector("#install-verdict"),
|
|
41
|
+
installMessage: document.querySelector("#install-message"),
|
|
42
|
+
installCommand: document.querySelector("#install-command"),
|
|
40
43
|
actions: document.querySelector("#actions"),
|
|
41
44
|
critical: document.querySelector("#critical-count"),
|
|
42
45
|
high: document.querySelector("#high-count"),
|
|
@@ -235,9 +238,36 @@ function renderResult(result) {
|
|
|
235
238
|
elements.downloadHtml.textContent = "Download HTML";
|
|
236
239
|
elements.downloadHtml.disabled = false;
|
|
237
240
|
|
|
241
|
+
renderInstallGate(result);
|
|
238
242
|
renderFindings(scan.findings ?? []);
|
|
239
243
|
}
|
|
240
244
|
|
|
245
|
+
function renderInstallGate(result) {
|
|
246
|
+
const scan = result.scan;
|
|
247
|
+
const policy = scan.policy ?? {};
|
|
248
|
+
const decision = policy.decision ?? "allow";
|
|
249
|
+
const target = installTargetFor(result);
|
|
250
|
+
const installName = safeInstallName(result.displayTarget ?? "skill");
|
|
251
|
+
const command = `npx @denial-web/clawguard install ${target} --to ./.agents/skills --name ${installName} --policy ${policy.preset ?? elements.policy.value}`;
|
|
252
|
+
|
|
253
|
+
elements.installCommand.textContent = command;
|
|
254
|
+
|
|
255
|
+
if (decision === "allow") {
|
|
256
|
+
elements.installVerdict.textContent = "Install allowed";
|
|
257
|
+
elements.installMessage.textContent = "ClawGuard would copy this skill into the destination after the policy gate passes.";
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (decision === "block") {
|
|
262
|
+
elements.installVerdict.textContent = "Install blocked";
|
|
263
|
+
elements.installMessage.textContent = "ClawGuard would stop before copying files. Review the findings before trusting this skill.";
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
elements.installVerdict.textContent = "Install paused";
|
|
268
|
+
elements.installMessage.textContent = "ClawGuard would require review, sandboxing, or approval before copying files.";
|
|
269
|
+
}
|
|
270
|
+
|
|
241
271
|
function renderFindings(findings) {
|
|
242
272
|
if (findings.length === 0) {
|
|
243
273
|
elements.findings.className = "findings empty-state";
|
|
@@ -342,6 +372,27 @@ function safeFilename(value) {
|
|
|
342
372
|
.slice(0, 80) || "scan";
|
|
343
373
|
}
|
|
344
374
|
|
|
375
|
+
function safeInstallName(value) {
|
|
376
|
+
return String(value || "skill")
|
|
377
|
+
.toLowerCase()
|
|
378
|
+
.replace(/[^a-z0-9_.-]/g, "-")
|
|
379
|
+
.replace(/-+/g, "-")
|
|
380
|
+
.replace(/^-|-$/g, "")
|
|
381
|
+
.slice(0, 60) || "skill";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function installTargetFor(result) {
|
|
385
|
+
if (result.source === "example" && result.example?.path) {
|
|
386
|
+
return result.example.path;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (result.source === "folder") {
|
|
390
|
+
return "./uploaded-skill";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return "./pasted-skill";
|
|
394
|
+
}
|
|
395
|
+
|
|
345
396
|
async function fetchJson(url, options = {}) {
|
|
346
397
|
const response = await fetch(url, {
|
|
347
398
|
headers: {
|
package/web/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>ClawGuard Web Demo</title>
|
|
7
|
-
<link rel="stylesheet" href="
|
|
7
|
+
<link rel="stylesheet" href="styles.css">
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<main class="shell">
|
|
@@ -75,6 +75,17 @@
|
|
|
75
75
|
</div>
|
|
76
76
|
</section>
|
|
77
77
|
|
|
78
|
+
<section class="install-panel" aria-label="Pre-install gate">
|
|
79
|
+
<div>
|
|
80
|
+
<p class="eyebrow">Pre-Install Gate</p>
|
|
81
|
+
<h2 id="install-verdict">Waiting for scan</h2>
|
|
82
|
+
<p id="install-message">Run a scan to see whether ClawGuard would install, pause, or block before files are trusted.</p>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="command-box">
|
|
85
|
+
<code id="install-command">npx @denial-web/clawguard install ./skill --to ./.agents/skills --policy governed</code>
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
|
|
78
89
|
<section class="metrics" aria-label="Finding summary">
|
|
79
90
|
<div><span>Critical</span><strong id="critical-count">0</strong></div>
|
|
80
91
|
<div><span>High</span><strong id="high-count">0</strong></div>
|
|
@@ -114,6 +125,6 @@
|
|
|
114
125
|
</section>
|
|
115
126
|
</section>
|
|
116
127
|
</main>
|
|
117
|
-
<script src="
|
|
128
|
+
<script src="app.js" type="module"></script>
|
|
118
129
|
</body>
|
|
119
130
|
</html>
|
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, .metrics div, .metadata-grid div, .finding-card, .empty-state {
|
|
267
|
+
.score-ring, .decision, .install-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);
|
|
@@ -302,6 +302,43 @@ input[type="file"] {
|
|
|
302
302
|
text-transform: uppercase;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
.install-panel {
|
|
306
|
+
display: grid;
|
|
307
|
+
grid-template-columns: minmax(220px, 0.8fr) minmax(0, 1.2fr);
|
|
308
|
+
gap: 14px;
|
|
309
|
+
align-items: stretch;
|
|
310
|
+
padding: 16px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.install-panel h2 {
|
|
314
|
+
margin-top: 5px;
|
|
315
|
+
font-size: 24px;
|
|
316
|
+
text-transform: uppercase;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#install-message {
|
|
320
|
+
margin-top: 6px;
|
|
321
|
+
color: var(--muted);
|
|
322
|
+
line-height: 1.4;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.command-box {
|
|
326
|
+
min-width: 0;
|
|
327
|
+
display: grid;
|
|
328
|
+
align-items: center;
|
|
329
|
+
border: 1px solid var(--line);
|
|
330
|
+
border-radius: 8px;
|
|
331
|
+
background: var(--field);
|
|
332
|
+
padding: 12px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.command-box code {
|
|
336
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
337
|
+
font-size: 13px;
|
|
338
|
+
line-height: 1.45;
|
|
339
|
+
overflow-wrap: anywhere;
|
|
340
|
+
}
|
|
341
|
+
|
|
305
342
|
.action-tags {
|
|
306
343
|
display: flex;
|
|
307
344
|
flex-wrap: wrap;
|
|
@@ -422,7 +459,7 @@ input[type="file"] {
|
|
|
422
459
|
}
|
|
423
460
|
|
|
424
461
|
@media (max-width: 900px) {
|
|
425
|
-
.workbench, .score-panel {
|
|
462
|
+
.workbench, .score-panel, .install-panel {
|
|
426
463
|
grid-template-columns: 1fr;
|
|
427
464
|
}
|
|
428
465
|
|