@denial-web/clawguard 0.1.3 → 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/src/cli.js +214 -7
- package/src/config.js +3 -1
- package/web/index.html +2 -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/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/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">
|
|
@@ -125,6 +125,6 @@
|
|
|
125
125
|
</section>
|
|
126
126
|
</section>
|
|
127
127
|
</main>
|
|
128
|
-
<script src="
|
|
128
|
+
<script src="app.js" type="module"></script>
|
|
129
129
|
</body>
|
|
130
130
|
</html>
|