@denial-web/clawguard 0.1.3 → 0.1.5

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 CHANGED
@@ -59,6 +59,21 @@ 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
+
71
+ If OpenClaw already has messaging configured, ClawGuard can hand the approval message to OpenClaw:
72
+
73
+ ```bash
74
+ npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
75
+ ```
76
+
62
77
  When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
63
78
 
64
79
  ```bash
@@ -13,6 +13,59 @@ 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
+
42
+ ### Approval Message Delivery
43
+
44
+ Option A uses OpenClaw's native messaging command after OpenClaw is already configured by the user:
45
+
46
+ ```bash
47
+ clawguard approvals send ./.clawguard/approvals.jsonl \
48
+ --via openclaw \
49
+ --channel telegram \
50
+ --target 123456789
51
+ ```
52
+
53
+ The adapter calls:
54
+
55
+ ```bash
56
+ openclaw message send --channel telegram --target 123456789 --message "<approval message>"
57
+ ```
58
+
59
+ This is the easiest path for OpenClaw users because ClawGuard does not need to own Telegram, WhatsApp, Slack, or Discord credentials.
60
+
61
+ Option B is planned as a ClawGuard-owned sender:
62
+
63
+ ```bash
64
+ clawguard approvals serve --telegram
65
+ ```
66
+
67
+ That path is better when the user wants the approval channel to stay independent from the agent runtime.
68
+
16
69
  ### Skill Folder Scan
17
70
 
18
71
  Command:
@@ -107,6 +160,20 @@ Current implementation:
107
160
  - Reports missing lockfile, missing origin metadata, version drift, source drift, invalid metadata, and unusual source URLs.
108
161
  - Adds a `clawhub` summary to JSON and HTML reports.
109
162
 
163
+ ## Hermes Agent Integration
164
+
165
+ Current wrapper pattern:
166
+
167
+ ```bash
168
+ clawguard hermes install ./candidate-skill \
169
+ --to ~/.hermes/skills \
170
+ --approval-out ./.clawguard/approvals.jsonl
171
+ ```
172
+
173
+ 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.
174
+
175
+ ClawGuard is independent and not affiliated with Hermes Agent or Nous Research.
176
+
110
177
  ### Metadata Comparison
111
178
 
112
179
  ClawGuard should compare:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denial-web/clawguard",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Explainable security scanner for OpenClaw-style skills and MCP tool configs.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { promises as fs } from "node:fs";
4
+ import { randomUUID } from "node:crypto";
5
+ import { execFile } from "node:child_process";
4
6
  import path from "node:path";
7
+ import { promisify } from "node:util";
5
8
  import { loadConfig, mergeConfig, parseSize } from "./config.js";
6
9
  import { policyShouldFail } from "./policy.js";
7
10
  import { createHtmlReport } from "./reporters/html.js";
@@ -9,6 +12,7 @@ import { createSarifReport } from "./reporters/sarif.js";
9
12
  import { scanTarget } from "./scanner.js";
10
13
 
11
14
  const args = process.argv.slice(2);
15
+ const execFileAsync = promisify(execFile);
12
16
  const failLevels = ["none", "low", "medium", "high", "critical"];
13
17
  const policyPresets = ["personal", "governed", "enterprise"];
14
18
  const policyFailDecisions = ["warn", "manual_review", "sandbox_required", "dual_approval", "block"];
@@ -25,18 +29,32 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
25
29
  process.exit(0);
26
30
  }
27
31
 
28
- const command = args[0];
32
+ const commandContext = parseCommand(args);
33
+ const { command, framework, optionValues } = commandContext;
29
34
 
30
- if (!["scan", "scan-workspace", "gate", "install"].includes(command)) {
35
+ if (!["scan", "scan-workspace", "gate", "install", "approvals-send"].includes(command)) {
31
36
  console.error(`Unknown command: ${command}`);
32
37
  printHelp();
33
38
  process.exit(1);
34
39
  }
35
40
 
36
41
  try {
37
- const cliOptions = parseOptions(args.slice(1));
42
+ if (command === "approvals-send") {
43
+ const sendOptions = parseApprovalSendOptions(optionValues);
44
+ const result = await sendApproval(sendOptions);
45
+ if (sendOptions.json) {
46
+ console.log(JSON.stringify(result, null, 2));
47
+ } else {
48
+ printApprovalSendResult(result);
49
+ }
50
+ process.exit(0);
51
+ }
52
+
53
+ const cliOptions = parseOptions(optionValues);
54
+ cliOptions.framework = framework;
38
55
  const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
39
56
  const options = mergeConfig(loadedConfig.config, cliOptions);
57
+ options.framework = framework;
40
58
  const result = await scanTarget(options.target, {
41
59
  maxFileSizeBytes: options.maxFileSizeBytes,
42
60
  maxFindingsPerRulePerFile: options.maxFindingsPerRulePerFile,
@@ -54,6 +72,8 @@ try {
54
72
  await writeReportFile(options.htmlPath, createHtmlReport(result));
55
73
  }
56
74
 
75
+ let exitCode;
76
+
57
77
  if (command === "install") {
58
78
  const install = await handleInstall(result, options);
59
79
  if (options.json) {
@@ -61,6 +81,7 @@ try {
61
81
  } else {
62
82
  printInstallResult(result, install);
63
83
  }
84
+ exitCode = installExitCode(result.policy.decision, install);
64
85
  } else if (command === "gate") {
65
86
  if (options.json) {
66
87
  console.log(JSON.stringify(createGateResult(result), null, 2));
@@ -73,7 +94,7 @@ try {
73
94
  printHumanResult(result, options);
74
95
  }
75
96
 
76
- process.exit(["gate", "install"].includes(command) ? gateExitCode(result.policy.decision) : shouldFail(result, options) ? 2 : 0);
97
+ process.exit(exitCode ?? (command === "gate" ? gateExitCode(result.policy.decision) : shouldFail(result, options) ? 2 : 0));
77
98
  } catch (error) {
78
99
  console.error(`${commandLabel(command)} failed: ${error.message}`);
79
100
  process.exit(1);
@@ -86,6 +107,9 @@ Usage:
86
107
  clawguard scan <path> [--json] [--policy <preset>] [--fail-on <level>]
87
108
  clawguard gate <path> [--json] [--policy <preset>]
88
109
  clawguard install <path> --to <dir> [--policy <preset>] [--dry-run]
110
+ clawguard openclaw install <path> --to <dir> [--approval-out <path>]
111
+ clawguard hermes install <path> --to <dir> [--approval-out <path>]
112
+ clawguard approvals send <approval.json|approvals.jsonl> --via openclaw --channel <name> --target <id>
89
113
  clawguard scan-workspace <path> [--json] [--policy <preset>]
90
114
  npm run scan -- <path>
91
115
 
@@ -107,6 +131,14 @@ Options:
107
131
  --to <dir> Install destination parent directory for install mode.
108
132
  --name <name> Install folder/file name. Defaults to the source basename.
109
133
  --dry-run Run install gate and show the destination without copying files.
134
+ --approval-out <path> Write a pending approval JSON request before copying.
135
+ Use .jsonl to append JSON lines for bot/daemon integrations.
136
+ --approval-mode <mode> Approval mode: non-allow, always. Default: non-allow.
137
+ --via <adapter> Approval send adapter. Currently: openclaw.
138
+ --channel <name> Messaging channel for approval send, such as telegram.
139
+ --target <id> Messaging target/chat id for approval send.
140
+ --sender-bin <path> Sender binary. Default for --via openclaw: openclaw.
141
+ --sender-arg <value> Extra argument before the generated sender command. Repeatable.
110
142
 
111
143
  Gate exit codes:
112
144
  0 = allow
@@ -117,6 +149,9 @@ Examples:
117
149
  npx @denial-web/clawguard gate ./skills/my-skill
118
150
  npx @denial-web/clawguard gate ./skills/my-skill --policy governed
119
151
  npx @denial-web/clawguard install ./skills/my-skill --to ./.agents/skills --policy governed
152
+ npx @denial-web/clawguard openclaw install ./skills/my-skill --to ./.agents/skills --approval-out ./.clawguard/approvals.jsonl
153
+ npx @denial-web/clawguard hermes install ./skills/my-skill --to ~/.hermes/skills --approval-out ./.clawguard/approvals.jsonl
154
+ npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
120
155
  npm run scan -- examples/risky-skill
121
156
  npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
122
157
  npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
@@ -197,6 +232,147 @@ function printHumanResult(result, options) {
197
232
  }
198
233
  }
199
234
 
235
+ function parseCommand(values) {
236
+ const rawCommand = values[0];
237
+
238
+ if (rawCommand === "approvals" && values[1] === "send") {
239
+ return {
240
+ command: "approvals-send",
241
+ framework: undefined,
242
+ optionValues: values.slice(2)
243
+ };
244
+ }
245
+
246
+ if (["openclaw", "hermes"].includes(rawCommand)) {
247
+ const nestedCommand = values[1];
248
+
249
+ if (!nestedCommand) {
250
+ return {
251
+ command: "",
252
+ framework: rawCommand,
253
+ optionValues: []
254
+ };
255
+ }
256
+
257
+ if (!["gate", "install"].includes(nestedCommand)) {
258
+ return {
259
+ command: `${rawCommand} ${nestedCommand}`,
260
+ framework: rawCommand,
261
+ optionValues: values.slice(2)
262
+ };
263
+ }
264
+
265
+ return {
266
+ command: nestedCommand,
267
+ framework: rawCommand,
268
+ optionValues: values.slice(2)
269
+ };
270
+ }
271
+
272
+ return {
273
+ command: rawCommand,
274
+ framework: undefined,
275
+ optionValues: values.slice(1)
276
+ };
277
+ }
278
+
279
+ async function sendApproval(options) {
280
+ const approval = await readApprovalRequest(options.approvalPath, options.id);
281
+ const message = String(approval.message ?? "").trim();
282
+
283
+ if (!message) {
284
+ throw new Error("Approval request has no message field.");
285
+ }
286
+
287
+ if (options.via !== "openclaw") {
288
+ throw new Error("Only --via openclaw is supported right now.");
289
+ }
290
+
291
+ const senderBin = options.senderBin ?? "openclaw";
292
+ const commandArgs = [
293
+ ...options.senderArgs,
294
+ "message",
295
+ "send",
296
+ "--channel",
297
+ options.channel,
298
+ "--target",
299
+ options.target,
300
+ "--message",
301
+ message
302
+ ];
303
+ const result = {
304
+ approval: {
305
+ id: approval.id,
306
+ status: approval.status,
307
+ decision: approval.decision,
308
+ risk: approval.risk,
309
+ framework: approval.framework
310
+ },
311
+ via: options.via,
312
+ channel: options.channel,
313
+ target: options.target,
314
+ senderBin,
315
+ command: [senderBin, ...commandArgs],
316
+ dryRun: options.dryRun,
317
+ sent: false,
318
+ stdout: "",
319
+ stderr: ""
320
+ };
321
+
322
+ if (options.dryRun) {
323
+ return result;
324
+ }
325
+
326
+ const output = await execFileAsync(senderBin, commandArgs, {
327
+ maxBuffer: 1024 * 1024
328
+ });
329
+
330
+ result.sent = true;
331
+ result.stdout = output.stdout;
332
+ result.stderr = output.stderr;
333
+ return result;
334
+ }
335
+
336
+ function printApprovalSendResult(result) {
337
+ console.log(`ClawGuard approval send: ${result.approval.id}`);
338
+ console.log(`Via: ${result.via}`);
339
+ console.log(`Channel: ${result.channel}`);
340
+ console.log(`Target: ${result.target}`);
341
+ console.log(`Decision: ${formatDecision(result.approval.decision ?? "unknown")}`);
342
+ console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
343
+ console.log(`Sent: ${result.sent ? "yes" : "no"}`);
344
+
345
+ if (result.dryRun) {
346
+ console.log(`Command: ${result.command.map(shellQuote).join(" ")}`);
347
+ }
348
+ }
349
+
350
+ async function readApprovalRequest(approvalPath, id) {
351
+ const resolvedPath = path.resolve(approvalPath);
352
+ const content = await fs.readFile(resolvedPath, "utf8");
353
+ const approvals = resolvedPath.endsWith(".jsonl")
354
+ ? content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
355
+ : [JSON.parse(content)];
356
+
357
+ if (approvals.length === 0) {
358
+ throw new Error(`No approval requests found in ${resolvedPath}`);
359
+ }
360
+
361
+ const approval = id
362
+ ? approvals.find((candidate) => candidate.id === id)
363
+ : approvals.at(-1);
364
+
365
+ if (!approval) {
366
+ throw new Error(`Approval request not found: ${id}`);
367
+ }
368
+
369
+ if (approval.schemaVersion !== "clawguard.approval.v1") {
370
+ throw new Error("Unsupported approval request schema.");
371
+ }
372
+
373
+ return approval;
374
+ }
375
+
200
376
  function printGateResult(result, options) {
201
377
  const decision = result.policy.decision;
202
378
  console.log(`ClawGuard gate: ${result.target}`);
@@ -243,10 +419,18 @@ async function handleInstall(result, options) {
243
419
  const install = {
244
420
  destination,
245
421
  dryRun: options.dryRun,
422
+ framework: options.framework,
246
423
  installed: false,
247
- skipped: decision !== "allow"
424
+ skipped: decision !== "allow",
425
+ approvalRequest: null
248
426
  };
249
427
 
428
+ if (shouldCreateApprovalRequest(decision, options)) {
429
+ install.approvalRequest = await writeApprovalRequest(result, install, options);
430
+ install.skipped = true;
431
+ return install;
432
+ }
433
+
250
434
  if (decision !== "allow") {
251
435
  return install;
252
436
  }
@@ -277,10 +461,13 @@ async function handleInstall(result, options) {
277
461
  function printInstallResult(result, install) {
278
462
  const decision = result.policy.decision;
279
463
  console.log(`ClawGuard install: ${result.target}`);
464
+ if (install.framework) {
465
+ console.log(`Framework: ${displayFramework(install.framework)}`);
466
+ }
280
467
  console.log(`Decision: ${formatDecision(decision)}`);
281
468
  console.log(`Risk: ${result.level.toUpperCase()} (${result.score}/100)`);
282
469
  console.log(`Policy: ${result.policy.preset}`);
283
- console.log(`Exit code: ${gateExitCode(decision)}`);
470
+ console.log(`Exit code: ${installExitCode(decision, install)}`);
284
471
  console.log(`Destination: ${install.destination ?? "not selected"}`);
285
472
  console.log(`Installed: ${install.installed ? "yes" : "no"}`);
286
473
 
@@ -292,7 +479,11 @@ function printInstallResult(result, install) {
292
479
  console.log(`Required actions: ${result.policy.requiredActions.join(", ")}`);
293
480
  }
294
481
 
295
- if (decision === "allow" && install.installed) {
482
+ if (install.approvalRequest) {
483
+ console.log(`Approval request: ${install.approvalRequest.path}`);
484
+ console.log(`Approval id: ${install.approvalRequest.id}`);
485
+ console.log("\nInstall result: pending user approval before copying files.");
486
+ } else if (decision === "allow" && install.installed) {
296
487
  console.log("\nInstall result: copied after passing the selected policy.");
297
488
  } else if (decision === "allow" && install.dryRun) {
298
489
  console.log("\nInstall result: dry run passed; no files were copied.");
@@ -308,10 +499,13 @@ function printInstallResult(result, install) {
308
499
  function createInstallResult(result, install) {
309
500
  return {
310
501
  ...createGateResult(result),
502
+ exitCode: installExitCode(result.policy.decision, install),
503
+ framework: install.framework,
311
504
  destination: install.destination,
312
505
  installed: install.installed,
313
506
  dryRun: install.dryRun,
314
- skipped: install.skipped
507
+ skipped: install.skipped,
508
+ approvalRequest: install.approvalRequest
315
509
  };
316
510
  }
317
511
 
@@ -350,6 +544,106 @@ function resolveInstallDestination(sourcePath, options) {
350
544
  return path.resolve(options.installDir, installName);
351
545
  }
352
546
 
547
+ function shouldCreateApprovalRequest(decision, options) {
548
+ if (!options.approvalOut) {
549
+ return false;
550
+ }
551
+
552
+ if (options.approvalMode === "always") {
553
+ return true;
554
+ }
555
+
556
+ return decision !== "allow";
557
+ }
558
+
559
+ async function writeApprovalRequest(result, install, options) {
560
+ const request = createApprovalRequest(result, install, options);
561
+ const outputPath = path.resolve(options.approvalOut);
562
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
563
+
564
+ if (outputPath.endsWith(".jsonl")) {
565
+ await fs.appendFile(outputPath, `${JSON.stringify(request)}\n`);
566
+ } else {
567
+ await fs.writeFile(outputPath, `${JSON.stringify(request, null, 2)}\n`, { flag: "wx" });
568
+ }
569
+
570
+ return {
571
+ id: request.id,
572
+ path: outputPath,
573
+ status: request.status,
574
+ message: request.message
575
+ };
576
+ }
577
+
578
+ function createApprovalRequest(result, install, options) {
579
+ const id = randomUUID();
580
+ const decision = result.policy.decision;
581
+ const framework = options.framework ?? "generic";
582
+ const target = path.resolve(options.target);
583
+ const topFindings = result.findings.slice(0, 5).map((finding) => ({
584
+ ruleId: finding.ruleId,
585
+ severity: finding.severity,
586
+ title: finding.title,
587
+ file: finding.file,
588
+ line: finding.line,
589
+ recommendation: finding.recommendation
590
+ }));
591
+
592
+ return {
593
+ schemaVersion: "clawguard.approval.v1",
594
+ id,
595
+ status: "pending",
596
+ createdAt: new Date().toISOString(),
597
+ framework,
598
+ target,
599
+ destination: install.destination,
600
+ decision,
601
+ risk: {
602
+ level: result.level,
603
+ score: result.score
604
+ },
605
+ policy: {
606
+ preset: result.policy.preset,
607
+ reason: result.policy.reason,
608
+ requiredActions: result.policy.requiredActions
609
+ },
610
+ install: {
611
+ dryRun: install.dryRun,
612
+ installed: false,
613
+ skipped: true
614
+ },
615
+ summary: result.summary,
616
+ findings: topFindings,
617
+ message: createApprovalMessage({
618
+ framework,
619
+ target,
620
+ destination: install.destination,
621
+ decision,
622
+ risk: result.level,
623
+ score: result.score,
624
+ requiredActions: result.policy.requiredActions,
625
+ findings: topFindings
626
+ })
627
+ };
628
+ }
629
+
630
+ function createApprovalMessage(details) {
631
+ const findingLines = details.findings.length === 0
632
+ ? "No findings were reported."
633
+ : details.findings.map((finding) => `- ${finding.severity.toUpperCase()}: ${finding.title}`).join("\n");
634
+
635
+ return [
636
+ `ClawGuard approval needed for ${displayFramework(details.framework)} skill install.`,
637
+ `Decision: ${formatDecision(details.decision)}`,
638
+ `Risk: ${details.risk.toUpperCase()} (${details.score}/100)`,
639
+ `Source: ${details.target}`,
640
+ `Destination: ${details.destination ?? "not selected"}`,
641
+ `Required actions: ${details.requiredActions.length > 0 ? details.requiredActions.join(", ") : "none"}`,
642
+ "Top findings:",
643
+ findingLines
644
+ ].join("\n");
645
+ }
646
+
353
647
  async function assertInstallableSource(sourcePath) {
354
648
  const stats = await fs.lstat(sourcePath);
355
649
 
@@ -404,10 +698,31 @@ function commandLabel(commandName) {
404
698
  return "Scan";
405
699
  }
406
700
 
701
+ function displayFramework(value) {
702
+ if (value === "openclaw") {
703
+ return "OpenClaw";
704
+ }
705
+
706
+ if (value === "hermes") {
707
+ return "Hermes Agent";
708
+ }
709
+
710
+ return "agent";
711
+ }
712
+
407
713
  function formatDecision(decision) {
408
714
  return decision.replaceAll("_", " ").toUpperCase();
409
715
  }
410
716
 
717
+ function shellQuote(value) {
718
+ const text = String(value);
719
+ if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) {
720
+ return text;
721
+ }
722
+
723
+ return `'${text.replaceAll("'", "'\\''")}'`;
724
+ }
725
+
411
726
  function gateExitCode(decision) {
412
727
  if (decision === "allow") {
413
728
  return 0;
@@ -420,6 +735,14 @@ function gateExitCode(decision) {
420
735
  return 1;
421
736
  }
422
737
 
738
+ function installExitCode(decision, install) {
739
+ if (install.approvalRequest) {
740
+ return 1;
741
+ }
742
+
743
+ return gateExitCode(decision);
744
+ }
745
+
423
746
  function parseOptions(values) {
424
747
  const options = {
425
748
  json: false,
@@ -434,6 +757,9 @@ function parseOptions(values) {
434
757
  installDir: undefined,
435
758
  installName: undefined,
436
759
  dryRun: false,
760
+ approvalOut: undefined,
761
+ approvalMode: "non-allow",
762
+ framework: undefined,
437
763
  target: "."
438
764
  };
439
765
  const paths = [];
@@ -527,6 +853,22 @@ function parseOptions(values) {
527
853
  continue;
528
854
  }
529
855
 
856
+ if (value === "--approval-out") {
857
+ options.approvalOut = requireNextValue(values, index, "--approval-out");
858
+ index += 1;
859
+ continue;
860
+ }
861
+
862
+ if (value === "--approval-mode") {
863
+ const mode = requireNextValue(values, index, "--approval-mode");
864
+ if (!["non-allow", "always"].includes(mode)) {
865
+ throw new Error("Invalid --approval-mode value. Use one of: non-allow, always");
866
+ }
867
+ options.approvalMode = mode;
868
+ index += 1;
869
+ continue;
870
+ }
871
+
530
872
  if (value.startsWith("--")) {
531
873
  throw new Error(`Unknown option: ${value}`);
532
874
  }
@@ -538,6 +880,97 @@ function parseOptions(values) {
538
880
  return options;
539
881
  }
540
882
 
883
+ function parseApprovalSendOptions(values) {
884
+ const options = {
885
+ approvalPath: undefined,
886
+ id: undefined,
887
+ via: "openclaw",
888
+ channel: undefined,
889
+ target: undefined,
890
+ senderBin: undefined,
891
+ senderArgs: [],
892
+ dryRun: false,
893
+ json: false
894
+ };
895
+ const paths = [];
896
+
897
+ for (let index = 0; index < values.length; index += 1) {
898
+ const value = values[index];
899
+
900
+ if (value === "--json") {
901
+ options.json = true;
902
+ continue;
903
+ }
904
+
905
+ if (value === "--dry-run") {
906
+ options.dryRun = true;
907
+ continue;
908
+ }
909
+
910
+ if (value === "--id") {
911
+ options.id = requireNextValue(values, index, "--id");
912
+ index += 1;
913
+ continue;
914
+ }
915
+
916
+ if (value === "--via") {
917
+ options.via = requireNextValue(values, index, "--via");
918
+ index += 1;
919
+ continue;
920
+ }
921
+
922
+ if (value === "--channel") {
923
+ options.channel = requireNextValue(values, index, "--channel");
924
+ index += 1;
925
+ continue;
926
+ }
927
+
928
+ if (value === "--target") {
929
+ options.target = requireNextValue(values, index, "--target");
930
+ index += 1;
931
+ continue;
932
+ }
933
+
934
+ if (value === "--sender-bin") {
935
+ options.senderBin = requireNextValue(values, index, "--sender-bin");
936
+ index += 1;
937
+ continue;
938
+ }
939
+
940
+ if (value === "--sender-arg") {
941
+ options.senderArgs.push(requireNextValue(values, index, "--sender-arg"));
942
+ index += 1;
943
+ continue;
944
+ }
945
+
946
+ if (value.startsWith("--")) {
947
+ throw new Error(`Unknown option: ${value}`);
948
+ }
949
+
950
+ paths.push(value);
951
+ }
952
+
953
+ options.approvalPath = paths[0];
954
+
955
+ if (!options.approvalPath) {
956
+ throw new Error("approvals send requires <approval.json|approvals.jsonl>.");
957
+ }
958
+
959
+ if (options.via !== "openclaw") {
960
+ throw new Error("Invalid --via value. Use: openclaw");
961
+ }
962
+
963
+ if (!options.channel) {
964
+ throw new Error("approvals send requires --channel <name>.");
965
+ }
966
+
967
+ if (!options.target) {
968
+ throw new Error("approvals send requires --target <id>.");
969
+ }
970
+
971
+ return options;
972
+ }
973
+
541
974
  async function writeReportFile(outputPath, content) {
542
975
  const resolvedPath = path.resolve(outputPath);
543
976
  await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
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="/styles.css">
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="/app.js" type="module"></script>
128
+ <script src="app.js" type="module"></script>
129
129
  </body>
130
130
  </html>