@denial-web/clawguard 0.1.7 → 0.1.9
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 +19 -0
- package/docs/INTEGRATION_SPEC.md +25 -0
- package/package.json +1 -1
- package/src/cli.js +506 -5
package/README.md
CHANGED
|
@@ -80,6 +80,25 @@ If you want ClawGuard to own the approval channel separately, send directly thro
|
|
|
80
80
|
TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
For an automatic bridge, keep a watcher running. It forwards each new pending approval once and stores sent approval ids in `./.clawguard/approvals.jsonl.sent.json` by default:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use `--once --dry-run` to verify the message flow without sending anything:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789 --once --dry-run
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Record the owner's decision in a durable decision log:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <approval-id> --decision approve --out ./.clawguard/decisions.jsonl
|
|
99
|
+
npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <approval-id> --decision deny --reason "Unexpected shell access" --out ./.clawguard/decisions.jsonl
|
|
100
|
+
```
|
|
101
|
+
|
|
83
102
|
When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
|
|
84
103
|
|
|
85
104
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -68,6 +68,31 @@ TELEGRAM_BOT_TOKEN=123456:token clawguard approvals send ./.clawguard/approvals.
|
|
|
68
68
|
|
|
69
69
|
That path is better when the user wants the approval channel to stay independent from the agent runtime. Use `--dry-run` first to verify the redacted endpoint and message payload before sending.
|
|
70
70
|
|
|
71
|
+
For long-running installs, ClawGuard can watch the approval queue and forward each new pending request once:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
TELEGRAM_BOT_TOKEN=123456:token clawguard approvals watch ./.clawguard/approvals.jsonl \
|
|
75
|
+
--via telegram \
|
|
76
|
+
--chat-id 123456789
|
|
77
|
+
```
|
|
78
|
+
|
|
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
|
+
|
|
81
|
+
### Approval Decisions
|
|
82
|
+
|
|
83
|
+
Owner decisions are stored separately from approval requests. This keeps the original scan evidence immutable and gives messaging bridges a simple append-only target:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
clawguard approvals decide ./.clawguard/approvals.jsonl \
|
|
87
|
+
--id <approval-id> \
|
|
88
|
+
--decision approve \
|
|
89
|
+
--actor owner \
|
|
90
|
+
--reason "Reviewed and acceptable" \
|
|
91
|
+
--out ./.clawguard/decisions.jsonl
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The decision log uses `schemaVersion: "clawguard.decision.v1"` and stores the approval id, normalized decision, actor, reason, target, destination, risk summary, policy summary, and source approval path. Later Telegram, WhatsApp, OpenClaw, or Hermes bridges should write this same decision format after parsing owner replies.
|
|
95
|
+
|
|
71
96
|
### Skill Folder Scan
|
|
72
97
|
|
|
73
98
|
Command:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -32,7 +32,15 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
|
32
32
|
const commandContext = parseCommand(args);
|
|
33
33
|
const { command, framework, optionValues } = commandContext;
|
|
34
34
|
|
|
35
|
-
if (![
|
|
35
|
+
if (![
|
|
36
|
+
"scan",
|
|
37
|
+
"scan-workspace",
|
|
38
|
+
"gate",
|
|
39
|
+
"install",
|
|
40
|
+
"approvals-send",
|
|
41
|
+
"approvals-watch",
|
|
42
|
+
"approvals-decide"
|
|
43
|
+
].includes(command)) {
|
|
36
44
|
console.error(`Unknown command: ${command}`);
|
|
37
45
|
printHelp();
|
|
38
46
|
process.exit(1);
|
|
@@ -50,6 +58,30 @@ try {
|
|
|
50
58
|
process.exit(0);
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
if (command === "approvals-watch") {
|
|
62
|
+
const watchOptions = parseApprovalWatchOptions(optionValues);
|
|
63
|
+
const result = await watchApprovals(watchOptions, {
|
|
64
|
+
onSend: watchOptions.json ? undefined : printApprovalWatchSend
|
|
65
|
+
});
|
|
66
|
+
if (watchOptions.json) {
|
|
67
|
+
console.log(JSON.stringify(result, null, 2));
|
|
68
|
+
} else {
|
|
69
|
+
printApprovalWatchResult(result);
|
|
70
|
+
}
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (command === "approvals-decide") {
|
|
75
|
+
const decisionOptions = parseApprovalDecisionOptions(optionValues);
|
|
76
|
+
const result = await decideApproval(decisionOptions);
|
|
77
|
+
if (decisionOptions.json) {
|
|
78
|
+
console.log(JSON.stringify(result, null, 2));
|
|
79
|
+
} else {
|
|
80
|
+
printApprovalDecisionResult(result);
|
|
81
|
+
}
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
53
85
|
const cliOptions = parseOptions(optionValues);
|
|
54
86
|
cliOptions.framework = framework;
|
|
55
87
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -111,6 +143,8 @@ Usage:
|
|
|
111
143
|
clawguard hermes install <path> --to <dir> [--approval-out <path>]
|
|
112
144
|
clawguard approvals send <approval.json|approvals.jsonl> --via openclaw --channel <name> --target <id>
|
|
113
145
|
clawguard approvals send <approval.json|approvals.jsonl> --via telegram --chat-id <id>
|
|
146
|
+
clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
|
|
147
|
+
clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
|
|
114
148
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
115
149
|
npm run scan -- <path>
|
|
116
150
|
|
|
@@ -142,6 +176,14 @@ Options:
|
|
|
142
176
|
--sender-arg <value> Extra argument before the generated sender command. Repeatable.
|
|
143
177
|
--bot-token <token> Telegram bot token. Default: TELEGRAM_BOT_TOKEN.
|
|
144
178
|
--chat-id <id> Telegram chat id. Alias for --target with --via telegram.
|
|
179
|
+
--interval <ms> Approval watch poll interval. Default: 2000.
|
|
180
|
+
--state <path> Approval watch sent-id state file.
|
|
181
|
+
--once Run approval watch once and exit.
|
|
182
|
+
--id <id> Approval id for send or decide.
|
|
183
|
+
--decision <value> Approval decision: approve, deny.
|
|
184
|
+
--out <path> Decision JSONL output file.
|
|
185
|
+
--actor <name> Decision actor. Default: local-user.
|
|
186
|
+
--reason <text> Decision reason.
|
|
145
187
|
|
|
146
188
|
Gate exit codes:
|
|
147
189
|
0 = allow
|
|
@@ -156,6 +198,8 @@ Examples:
|
|
|
156
198
|
npx @denial-web/clawguard hermes install ./skills/my-skill --to ~/.hermes/skills --approval-out ./.clawguard/approvals.jsonl
|
|
157
199
|
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
|
|
158
200
|
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
201
|
+
npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
202
|
+
npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
|
|
159
203
|
npm run scan -- examples/risky-skill
|
|
160
204
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
161
205
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -247,6 +291,22 @@ function parseCommand(values) {
|
|
|
247
291
|
};
|
|
248
292
|
}
|
|
249
293
|
|
|
294
|
+
if (rawCommand === "approvals" && values[1] === "watch") {
|
|
295
|
+
return {
|
|
296
|
+
command: "approvals-watch",
|
|
297
|
+
framework: undefined,
|
|
298
|
+
optionValues: values.slice(2)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (rawCommand === "approvals" && values[1] === "decide") {
|
|
303
|
+
return {
|
|
304
|
+
command: "approvals-decide",
|
|
305
|
+
framework: undefined,
|
|
306
|
+
optionValues: values.slice(2)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
250
310
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
251
311
|
const nestedCommand = values[1];
|
|
252
312
|
|
|
@@ -282,6 +342,10 @@ function parseCommand(values) {
|
|
|
282
342
|
|
|
283
343
|
async function sendApproval(options) {
|
|
284
344
|
const approval = await readApprovalRequest(options.approvalPath, options.id);
|
|
345
|
+
return sendApprovalRequest(approval, options);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function sendApprovalRequest(approval, options) {
|
|
285
349
|
const message = String(approval.message ?? "").trim();
|
|
286
350
|
|
|
287
351
|
if (!message) {
|
|
@@ -341,6 +405,113 @@ async function sendApproval(options) {
|
|
|
341
405
|
return result;
|
|
342
406
|
}
|
|
343
407
|
|
|
408
|
+
async function watchApprovals(options, hooks = {}) {
|
|
409
|
+
const statePath = path.resolve(options.statePath ?? `${options.approvalPath}.sent.json`);
|
|
410
|
+
const persistedIds = await readApprovalWatchState(statePath);
|
|
411
|
+
const sessionIds = new Set();
|
|
412
|
+
const result = {
|
|
413
|
+
approvalPath: path.resolve(options.approvalPath),
|
|
414
|
+
statePath,
|
|
415
|
+
once: options.once,
|
|
416
|
+
intervalMs: options.intervalMs,
|
|
417
|
+
dryRun: options.dryRun,
|
|
418
|
+
checked: 0,
|
|
419
|
+
matched: 0,
|
|
420
|
+
sent: 0,
|
|
421
|
+
skipped: 0,
|
|
422
|
+
deliveries: []
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
do {
|
|
426
|
+
const approvals = await readApprovalRequestsIfPresent(options.approvalPath);
|
|
427
|
+
result.checked += approvals.length;
|
|
428
|
+
|
|
429
|
+
for (const approval of approvals) {
|
|
430
|
+
if (approval.status !== "pending") {
|
|
431
|
+
result.skipped += 1;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!approval.id) {
|
|
436
|
+
result.skipped += 1;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (persistedIds.has(approval.id) || sessionIds.has(approval.id)) {
|
|
441
|
+
result.skipped += 1;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
result.matched += 1;
|
|
446
|
+
const delivery = await sendApprovalRequest(approval, options);
|
|
447
|
+
result.deliveries.push(delivery);
|
|
448
|
+
sessionIds.add(approval.id);
|
|
449
|
+
|
|
450
|
+
if (delivery.sent) {
|
|
451
|
+
result.sent += 1;
|
|
452
|
+
persistedIds.add(approval.id);
|
|
453
|
+
await writeApprovalWatchState(statePath, persistedIds);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (hooks.onSend) {
|
|
457
|
+
hooks.onSend(delivery);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (options.once) {
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
await sleep(options.intervalMs);
|
|
466
|
+
} while (true);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function decideApproval(options) {
|
|
470
|
+
const approval = await readApprovalRequest(options.approvalPath, options.id);
|
|
471
|
+
const outputPath = path.resolve(options.outPath ?? `${options.approvalPath}.decisions.jsonl`);
|
|
472
|
+
const decision = createApprovalDecision(approval, options);
|
|
473
|
+
|
|
474
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
475
|
+
await fs.appendFile(outputPath, `${JSON.stringify(decision)}\n`);
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
approval: {
|
|
479
|
+
id: approval.id,
|
|
480
|
+
status: approval.status,
|
|
481
|
+
decision: approval.decision,
|
|
482
|
+
risk: approval.risk,
|
|
483
|
+
framework: approval.framework
|
|
484
|
+
},
|
|
485
|
+
outputPath,
|
|
486
|
+
decision
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function createApprovalDecision(approval, options) {
|
|
491
|
+
const decision = normalizeApprovalDecision(options.decision);
|
|
492
|
+
const status = decision === "approve" ? "approved" : "denied";
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
schemaVersion: "clawguard.decision.v1",
|
|
496
|
+
id: randomUUID(),
|
|
497
|
+
approvalId: approval.id,
|
|
498
|
+
status,
|
|
499
|
+
decision,
|
|
500
|
+
decidedAt: new Date().toISOString(),
|
|
501
|
+
actor: options.actor,
|
|
502
|
+
reason: options.reason,
|
|
503
|
+
framework: approval.framework,
|
|
504
|
+
target: approval.target,
|
|
505
|
+
destination: approval.destination,
|
|
506
|
+
risk: approval.risk,
|
|
507
|
+
policy: approval.policy,
|
|
508
|
+
source: {
|
|
509
|
+
path: path.resolve(options.approvalPath),
|
|
510
|
+
approvalCreatedAt: approval.createdAt
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
344
515
|
async function sendTelegramApproval(approval, message, options) {
|
|
345
516
|
const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
346
517
|
|
|
@@ -421,12 +592,41 @@ function printApprovalSendResult(result) {
|
|
|
421
592
|
}
|
|
422
593
|
}
|
|
423
594
|
|
|
595
|
+
function printApprovalWatchSend(result) {
|
|
596
|
+
console.log(`ClawGuard approval watch sent: ${result.approval.id}`);
|
|
597
|
+
console.log(`Via: ${result.via}`);
|
|
598
|
+
console.log(`Target: ${result.target}`);
|
|
599
|
+
console.log(`Sent: ${result.sent ? "yes" : "no"}`);
|
|
600
|
+
if (result.dryRun && result.endpoint) {
|
|
601
|
+
console.log(`Endpoint: ${result.endpoint}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function printApprovalWatchResult(result) {
|
|
606
|
+
console.log(`ClawGuard approval watch: ${result.approvalPath}`);
|
|
607
|
+
console.log(`State: ${result.statePath}`);
|
|
608
|
+
console.log(`Once: ${result.once ? "yes" : "no"}`);
|
|
609
|
+
console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
|
|
610
|
+
console.log(`Checked: ${result.checked}`);
|
|
611
|
+
console.log(`Matched pending: ${result.matched}`);
|
|
612
|
+
console.log(`Sent: ${result.sent}`);
|
|
613
|
+
console.log(`Skipped: ${result.skipped}`);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function printApprovalDecisionResult(result) {
|
|
617
|
+
console.log(`ClawGuard approval decision: ${result.approval.id}`);
|
|
618
|
+
console.log(`Decision: ${formatDecision(result.decision.decision)}`);
|
|
619
|
+
console.log(`Status: ${result.decision.status}`);
|
|
620
|
+
console.log(`Actor: ${result.decision.actor}`);
|
|
621
|
+
if (result.decision.reason) {
|
|
622
|
+
console.log(`Reason: ${result.decision.reason}`);
|
|
623
|
+
}
|
|
624
|
+
console.log(`Output: ${result.outputPath}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
424
627
|
async function readApprovalRequest(approvalPath, id) {
|
|
425
628
|
const resolvedPath = path.resolve(approvalPath);
|
|
426
|
-
const
|
|
427
|
-
const approvals = resolvedPath.endsWith(".jsonl")
|
|
428
|
-
? content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
|
|
429
|
-
: [JSON.parse(content)];
|
|
629
|
+
const approvals = await readApprovalRequests(resolvedPath);
|
|
430
630
|
|
|
431
631
|
if (approvals.length === 0) {
|
|
432
632
|
throw new Error(`No approval requests found in ${resolvedPath}`);
|
|
@@ -447,6 +647,59 @@ async function readApprovalRequest(approvalPath, id) {
|
|
|
447
647
|
return approval;
|
|
448
648
|
}
|
|
449
649
|
|
|
650
|
+
async function readApprovalRequestsIfPresent(approvalPath) {
|
|
651
|
+
const resolvedPath = path.resolve(approvalPath);
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
return await readApprovalRequests(resolvedPath);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if (error.code === "ENOENT") {
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function readApprovalRequests(resolvedPath) {
|
|
665
|
+
const content = await fs.readFile(resolvedPath, "utf8");
|
|
666
|
+
const approvals = resolvedPath.endsWith(".jsonl")
|
|
667
|
+
? content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
|
|
668
|
+
: [JSON.parse(content)];
|
|
669
|
+
|
|
670
|
+
for (const approval of approvals) {
|
|
671
|
+
if (approval.schemaVersion !== "clawguard.approval.v1") {
|
|
672
|
+
throw new Error("Unsupported approval request schema.");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return approvals;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async function readApprovalWatchState(statePath) {
|
|
680
|
+
try {
|
|
681
|
+
const content = await fs.readFile(statePath, "utf8");
|
|
682
|
+
const state = JSON.parse(content);
|
|
683
|
+
const ids = Array.isArray(state) ? state : state.sentIds;
|
|
684
|
+
return new Set(Array.isArray(ids) ? ids : []);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
if (error.code === "ENOENT") {
|
|
687
|
+
return new Set();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function writeApprovalWatchState(statePath, sentIds) {
|
|
695
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
696
|
+
await fs.writeFile(statePath, `${JSON.stringify({
|
|
697
|
+
schemaVersion: "clawguard.approval-watch-state.v1",
|
|
698
|
+
updatedAt: new Date().toISOString(),
|
|
699
|
+
sentIds: [...sentIds].sort()
|
|
700
|
+
}, null, 2)}\n`);
|
|
701
|
+
}
|
|
702
|
+
|
|
450
703
|
function printGateResult(result, options) {
|
|
451
704
|
const decision = result.policy.decision;
|
|
452
705
|
console.log(`ClawGuard gate: ${result.target}`);
|
|
@@ -761,6 +1014,18 @@ async function assertDestinationAvailable(destination) {
|
|
|
761
1014
|
}
|
|
762
1015
|
|
|
763
1016
|
function commandLabel(commandName) {
|
|
1017
|
+
if (commandName === "approvals-send") {
|
|
1018
|
+
return "Approval send";
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (commandName === "approvals-watch") {
|
|
1022
|
+
return "Approval watch";
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (commandName === "approvals-decide") {
|
|
1026
|
+
return "Approval decision";
|
|
1027
|
+
}
|
|
1028
|
+
|
|
764
1029
|
if (commandName === "gate") {
|
|
765
1030
|
return "Gate";
|
|
766
1031
|
}
|
|
@@ -801,6 +1066,24 @@ function redactTelegramToken(value) {
|
|
|
801
1066
|
return String(value).replace(/\/bot[^/]+\/sendMessage$/, "/bot<redacted>/sendMessage");
|
|
802
1067
|
}
|
|
803
1068
|
|
|
1069
|
+
function sleep(ms) {
|
|
1070
|
+
return new Promise((resolve) => {
|
|
1071
|
+
setTimeout(resolve, ms);
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function normalizeApprovalDecision(value) {
|
|
1076
|
+
if (value === "approved") {
|
|
1077
|
+
return "approve";
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (value === "denied") {
|
|
1081
|
+
return "deny";
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return value;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
804
1087
|
function gateExitCode(decision) {
|
|
805
1088
|
if (decision === "allow") {
|
|
806
1089
|
return 0;
|
|
@@ -1079,6 +1362,224 @@ function parseApprovalSendOptions(values) {
|
|
|
1079
1362
|
return options;
|
|
1080
1363
|
}
|
|
1081
1364
|
|
|
1365
|
+
function parseApprovalWatchOptions(values) {
|
|
1366
|
+
const options = {
|
|
1367
|
+
approvalPath: undefined,
|
|
1368
|
+
via: "telegram",
|
|
1369
|
+
channel: undefined,
|
|
1370
|
+
target: undefined,
|
|
1371
|
+
chatId: undefined,
|
|
1372
|
+
botToken: undefined,
|
|
1373
|
+
telegramApiBase: undefined,
|
|
1374
|
+
senderBin: undefined,
|
|
1375
|
+
senderArgs: [],
|
|
1376
|
+
dryRun: false,
|
|
1377
|
+
json: false,
|
|
1378
|
+
once: false,
|
|
1379
|
+
intervalMs: 2000,
|
|
1380
|
+
statePath: undefined
|
|
1381
|
+
};
|
|
1382
|
+
const paths = [];
|
|
1383
|
+
|
|
1384
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1385
|
+
const value = values[index];
|
|
1386
|
+
|
|
1387
|
+
if (value === "--json") {
|
|
1388
|
+
options.json = true;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (value === "--dry-run") {
|
|
1393
|
+
options.dryRun = true;
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
if (value === "--once") {
|
|
1398
|
+
options.once = true;
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (value === "--interval") {
|
|
1403
|
+
const interval = Number.parseInt(requireNextValue(values, index, "--interval"), 10);
|
|
1404
|
+
if (!Number.isSafeInteger(interval) || interval < 250) {
|
|
1405
|
+
throw new Error("--interval must be an integer of at least 250 milliseconds.");
|
|
1406
|
+
}
|
|
1407
|
+
options.intervalMs = interval;
|
|
1408
|
+
index += 1;
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (value === "--state") {
|
|
1413
|
+
options.statePath = requireNextValue(values, index, "--state");
|
|
1414
|
+
index += 1;
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
if (value === "--via") {
|
|
1419
|
+
options.via = requireNextValue(values, index, "--via");
|
|
1420
|
+
index += 1;
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (value === "--channel") {
|
|
1425
|
+
options.channel = requireNextValue(values, index, "--channel");
|
|
1426
|
+
index += 1;
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (value === "--target") {
|
|
1431
|
+
options.target = requireNextValue(values, index, "--target");
|
|
1432
|
+
index += 1;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
if (value === "--chat-id") {
|
|
1437
|
+
options.chatId = requireNextValue(values, index, "--chat-id");
|
|
1438
|
+
index += 1;
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (value === "--bot-token") {
|
|
1443
|
+
options.botToken = requireNextValue(values, index, "--bot-token");
|
|
1444
|
+
index += 1;
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if (value === "--telegram-api-base") {
|
|
1449
|
+
options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
|
|
1450
|
+
index += 1;
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (value === "--sender-bin") {
|
|
1455
|
+
options.senderBin = requireNextValue(values, index, "--sender-bin");
|
|
1456
|
+
index += 1;
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (value === "--sender-arg") {
|
|
1461
|
+
options.senderArgs.push(requireNextValue(values, index, "--sender-arg"));
|
|
1462
|
+
index += 1;
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (value.startsWith("--")) {
|
|
1467
|
+
throw new Error(`Unknown option: ${value}`);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
paths.push(value);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
options.approvalPath = paths[0];
|
|
1474
|
+
|
|
1475
|
+
if (!options.approvalPath) {
|
|
1476
|
+
throw new Error("approvals watch requires <approval.jsonl>.");
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (!["openclaw", "telegram"].includes(options.via)) {
|
|
1480
|
+
throw new Error("Invalid --via value. Use one of: openclaw, telegram");
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if (options.via === "openclaw" && !options.channel) {
|
|
1484
|
+
throw new Error("approvals watch --via openclaw requires --channel <name>.");
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (options.via === "openclaw" && !options.target) {
|
|
1488
|
+
throw new Error("approvals watch --via openclaw requires --target <id>.");
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (options.via === "telegram") {
|
|
1492
|
+
options.chatId = options.chatId ?? options.target;
|
|
1493
|
+
if (!options.chatId) {
|
|
1494
|
+
throw new Error("approvals watch --via telegram requires --chat-id <id>.");
|
|
1495
|
+
}
|
|
1496
|
+
options.channel = "telegram";
|
|
1497
|
+
options.target = options.chatId;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
return options;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function parseApprovalDecisionOptions(values) {
|
|
1504
|
+
const options = {
|
|
1505
|
+
approvalPath: undefined,
|
|
1506
|
+
id: undefined,
|
|
1507
|
+
decision: undefined,
|
|
1508
|
+
outPath: undefined,
|
|
1509
|
+
actor: "local-user",
|
|
1510
|
+
reason: undefined,
|
|
1511
|
+
json: false
|
|
1512
|
+
};
|
|
1513
|
+
const paths = [];
|
|
1514
|
+
|
|
1515
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1516
|
+
const value = values[index];
|
|
1517
|
+
|
|
1518
|
+
if (value === "--json") {
|
|
1519
|
+
options.json = true;
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (value === "--id") {
|
|
1524
|
+
options.id = requireNextValue(values, index, "--id");
|
|
1525
|
+
index += 1;
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (value === "--decision") {
|
|
1530
|
+
options.decision = requireNextValue(values, index, "--decision");
|
|
1531
|
+
index += 1;
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
if (value === "--out") {
|
|
1536
|
+
options.outPath = requireNextValue(values, index, "--out");
|
|
1537
|
+
index += 1;
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
if (value === "--actor") {
|
|
1542
|
+
options.actor = requireNextValue(values, index, "--actor");
|
|
1543
|
+
index += 1;
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
if (value === "--reason") {
|
|
1548
|
+
options.reason = requireNextValue(values, index, "--reason");
|
|
1549
|
+
index += 1;
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
if (value.startsWith("--")) {
|
|
1554
|
+
throw new Error(`Unknown option: ${value}`);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
paths.push(value);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
options.approvalPath = paths[0];
|
|
1561
|
+
|
|
1562
|
+
if (!options.approvalPath) {
|
|
1563
|
+
throw new Error("approvals decide requires <approval.json|approvals.jsonl>.");
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
if (!options.id) {
|
|
1567
|
+
throw new Error("approvals decide requires --id <id>.");
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (!options.decision) {
|
|
1571
|
+
throw new Error("approvals decide requires --decision approve|deny.");
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
options.decision = normalizeApprovalDecision(options.decision);
|
|
1575
|
+
|
|
1576
|
+
if (!["approve", "deny"].includes(options.decision)) {
|
|
1577
|
+
throw new Error("Invalid --decision value. Use one of: approve, deny");
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
return options;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1082
1583
|
async function writeReportFile(outputPath, content) {
|
|
1083
1584
|
const resolvedPath = path.resolve(outputPath);
|
|
1084
1585
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|