@denial-web/clawguard 0.1.8 → 0.1.10
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 +22 -0
- package/docs/INTEGRATION_SPEC.md +33 -0
- package/package.json +1 -1
- package/src/cli.js +501 -1
package/README.md
CHANGED
|
@@ -92,6 +92,28 @@ Use `--once --dry-run` to verify the message flow without sending anything:
|
|
|
92
92
|
TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789 --once --dry-run
|
|
93
93
|
```
|
|
94
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
|
+
|
|
102
|
+
To turn Telegram replies into decision records, ask the owner to reply with one of:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
approve <approval-id> optional reason
|
|
106
|
+
deny <approval-id> optional reason
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then poll Telegram updates:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The poller records the Telegram update offset in `./.clawguard/decisions.jsonl.telegram-state.json` by default so the same reply is not processed again.
|
|
116
|
+
|
|
95
117
|
When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
|
|
96
118
|
|
|
97
119
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -78,6 +78,39 @@ TELEGRAM_BOT_TOKEN=123456:token clawguard approvals watch ./.clawguard/approvals
|
|
|
78
78
|
|
|
79
79
|
The watcher keeps search and discovery unrestricted. It only reacts after a guarded install writes a pending approval request. By default it records sent ids in `./.clawguard/approvals.jsonl.sent.json`, so restarting the bridge does not resend the same request. Use `--once --dry-run` for setup checks and CI smoke tests.
|
|
80
80
|
|
|
81
|
+
### 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
|
+
|
|
96
|
+
### Telegram Reply Polling
|
|
97
|
+
|
|
98
|
+
The first reply bridge is Telegram polling. Owners can respond to the approval message with:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
approve <approval-id> optional reason
|
|
102
|
+
deny <approval-id> optional reason
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Command:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
TELEGRAM_BOT_TOKEN=123456:token clawguard approvals poll-telegram ./.clawguard/approvals.jsonl \
|
|
109
|
+
--decisions ./.clawguard/decisions.jsonl
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The poller calls Telegram `getUpdates`, parses approval commands, looks up the referenced approval request, and appends a `clawguard.decision.v1` decision. It records the next Telegram update offset in `./.clawguard/decisions.jsonl.telegram-state.json` by default. For tests and offline replay, `--telegram-updates-file <path>` can read a captured Telegram-style update payload instead of calling the Telegram API.
|
|
113
|
+
|
|
81
114
|
### Skill Folder Scan
|
|
82
115
|
|
|
83
116
|
Command:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -32,7 +32,16 @@ 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
|
+
"approvals-poll-telegram"
|
|
44
|
+
].includes(command)) {
|
|
36
45
|
console.error(`Unknown command: ${command}`);
|
|
37
46
|
printHelp();
|
|
38
47
|
process.exit(1);
|
|
@@ -63,6 +72,28 @@ try {
|
|
|
63
72
|
process.exit(0);
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
if (command === "approvals-decide") {
|
|
76
|
+
const decisionOptions = parseApprovalDecisionOptions(optionValues);
|
|
77
|
+
const result = await decideApproval(decisionOptions);
|
|
78
|
+
if (decisionOptions.json) {
|
|
79
|
+
console.log(JSON.stringify(result, null, 2));
|
|
80
|
+
} else {
|
|
81
|
+
printApprovalDecisionResult(result);
|
|
82
|
+
}
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (command === "approvals-poll-telegram") {
|
|
87
|
+
const pollOptions = parseApprovalTelegramPollOptions(optionValues);
|
|
88
|
+
const result = await pollTelegramApprovals(pollOptions);
|
|
89
|
+
if (pollOptions.json) {
|
|
90
|
+
console.log(JSON.stringify(result, null, 2));
|
|
91
|
+
} else {
|
|
92
|
+
printApprovalTelegramPollResult(result);
|
|
93
|
+
}
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
const cliOptions = parseOptions(optionValues);
|
|
67
98
|
cliOptions.framework = framework;
|
|
68
99
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -125,6 +156,8 @@ Usage:
|
|
|
125
156
|
clawguard approvals send <approval.json|approvals.jsonl> --via openclaw --channel <name> --target <id>
|
|
126
157
|
clawguard approvals send <approval.json|approvals.jsonl> --via telegram --chat-id <id>
|
|
127
158
|
clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
|
|
159
|
+
clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
|
|
160
|
+
clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
|
|
128
161
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
129
162
|
npm run scan -- <path>
|
|
130
163
|
|
|
@@ -159,6 +192,15 @@ Options:
|
|
|
159
192
|
--interval <ms> Approval watch poll interval. Default: 2000.
|
|
160
193
|
--state <path> Approval watch sent-id state file.
|
|
161
194
|
--once Run approval watch once and exit.
|
|
195
|
+
--id <id> Approval id for send or decide.
|
|
196
|
+
--decision <value> Approval decision: approve, deny.
|
|
197
|
+
--out <path> Decision JSONL output file.
|
|
198
|
+
--decisions <path> Decision JSONL output file for reply polling.
|
|
199
|
+
--actor <name> Decision actor. Default: local-user.
|
|
200
|
+
--reason <text> Decision reason.
|
|
201
|
+
--offset-state <path> Telegram update offset state file.
|
|
202
|
+
--telegram-updates-file <path>
|
|
203
|
+
Read Telegram updates from a JSON file for tests or offline replay.
|
|
162
204
|
|
|
163
205
|
Gate exit codes:
|
|
164
206
|
0 = allow
|
|
@@ -174,6 +216,8 @@ Examples:
|
|
|
174
216
|
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
|
|
175
217
|
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
176
218
|
npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
219
|
+
npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
|
|
220
|
+
npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
|
|
177
221
|
npm run scan -- examples/risky-skill
|
|
178
222
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
179
223
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -273,6 +317,22 @@ function parseCommand(values) {
|
|
|
273
317
|
};
|
|
274
318
|
}
|
|
275
319
|
|
|
320
|
+
if (rawCommand === "approvals" && values[1] === "decide") {
|
|
321
|
+
return {
|
|
322
|
+
command: "approvals-decide",
|
|
323
|
+
framework: undefined,
|
|
324
|
+
optionValues: values.slice(2)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (rawCommand === "approvals" && values[1] === "poll-telegram") {
|
|
329
|
+
return {
|
|
330
|
+
command: "approvals-poll-telegram",
|
|
331
|
+
framework: undefined,
|
|
332
|
+
optionValues: values.slice(2)
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
276
336
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
277
337
|
const nestedCommand = values[1];
|
|
278
338
|
|
|
@@ -432,6 +492,200 @@ async function watchApprovals(options, hooks = {}) {
|
|
|
432
492
|
} while (true);
|
|
433
493
|
}
|
|
434
494
|
|
|
495
|
+
async function decideApproval(options) {
|
|
496
|
+
const approval = await readApprovalRequest(options.approvalPath, options.id);
|
|
497
|
+
const outputPath = path.resolve(options.outPath ?? `${options.approvalPath}.decisions.jsonl`);
|
|
498
|
+
const decision = createApprovalDecision(approval, options);
|
|
499
|
+
|
|
500
|
+
await appendApprovalDecision(outputPath, decision);
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
approval: {
|
|
504
|
+
id: approval.id,
|
|
505
|
+
status: approval.status,
|
|
506
|
+
decision: approval.decision,
|
|
507
|
+
risk: approval.risk,
|
|
508
|
+
framework: approval.framework
|
|
509
|
+
},
|
|
510
|
+
outputPath,
|
|
511
|
+
decision
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function appendApprovalDecision(outputPath, decision) {
|
|
516
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
517
|
+
await fs.appendFile(outputPath, `${JSON.stringify(decision)}\n`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function pollTelegramApprovals(options) {
|
|
521
|
+
const offsetStatePath = path.resolve(options.offsetStatePath ?? `${options.decisionsPath}.telegram-state.json`);
|
|
522
|
+
const offset = await readTelegramOffsetState(offsetStatePath);
|
|
523
|
+
const updates = await fetchTelegramUpdates(options, offset);
|
|
524
|
+
const outputPath = path.resolve(options.decisionsPath);
|
|
525
|
+
const result = {
|
|
526
|
+
approvalPath: path.resolve(options.approvalPath),
|
|
527
|
+
decisionsPath: outputPath,
|
|
528
|
+
offsetStatePath,
|
|
529
|
+
dryRun: options.dryRun,
|
|
530
|
+
checked: updates.length,
|
|
531
|
+
commands: 0,
|
|
532
|
+
decided: 0,
|
|
533
|
+
skipped: 0,
|
|
534
|
+
errors: [],
|
|
535
|
+
decisions: [],
|
|
536
|
+
nextOffset: offset
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
for (const update of updates) {
|
|
540
|
+
const parsed = parseTelegramApprovalUpdate(update);
|
|
541
|
+
|
|
542
|
+
if (!parsed) {
|
|
543
|
+
result.skipped += 1;
|
|
544
|
+
result.nextOffset = nextTelegramOffset(result.nextOffset, update.update_id);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
result.commands += 1;
|
|
549
|
+
result.nextOffset = nextTelegramOffset(result.nextOffset, update.update_id);
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const approval = await readApprovalRequest(options.approvalPath, parsed.approvalId);
|
|
553
|
+
const decision = createApprovalDecision(approval, {
|
|
554
|
+
approvalPath: options.approvalPath,
|
|
555
|
+
decision: parsed.decision,
|
|
556
|
+
actor: parsed.actor,
|
|
557
|
+
reason: parsed.reason
|
|
558
|
+
});
|
|
559
|
+
result.decisions.push(decision);
|
|
560
|
+
|
|
561
|
+
if (!options.dryRun) {
|
|
562
|
+
await appendApprovalDecision(outputPath, decision);
|
|
563
|
+
result.decided += 1;
|
|
564
|
+
}
|
|
565
|
+
} catch (error) {
|
|
566
|
+
result.errors.push({
|
|
567
|
+
updateId: update.update_id,
|
|
568
|
+
approvalId: parsed.approvalId,
|
|
569
|
+
message: error.message
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (!options.dryRun && result.nextOffset !== offset) {
|
|
575
|
+
await writeTelegramOffsetState(offsetStatePath, result.nextOffset);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function fetchTelegramUpdates(options, offset) {
|
|
582
|
+
if (options.telegramUpdatesPath) {
|
|
583
|
+
const content = await fs.readFile(path.resolve(options.telegramUpdatesPath), "utf8");
|
|
584
|
+
const payload = JSON.parse(content);
|
|
585
|
+
const updates = Array.isArray(payload) ? payload : payload.result;
|
|
586
|
+
|
|
587
|
+
if (!Array.isArray(updates)) {
|
|
588
|
+
throw new Error("Telegram updates file must be an array or an object with a result array.");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return updates.filter((update) => !offset || !Number.isSafeInteger(update.update_id) || update.update_id >= offset);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
595
|
+
|
|
596
|
+
if (!botToken) {
|
|
597
|
+
throw new Error("Telegram poll requires --bot-token or TELEGRAM_BOT_TOKEN.");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const apiBase = options.telegramApiBase ?? "https://api.telegram.org";
|
|
601
|
+
const endpoint = new URL(`${apiBase.replace(/\/$/, "")}/bot${botToken}/getUpdates`);
|
|
602
|
+
endpoint.searchParams.set("timeout", String(options.timeoutSeconds));
|
|
603
|
+
|
|
604
|
+
if (offset) {
|
|
605
|
+
endpoint.searchParams.set("offset", String(offset));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const response = await fetch(endpoint);
|
|
609
|
+
const text = await response.text();
|
|
610
|
+
let payload;
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
payload = text ? JSON.parse(text) : null;
|
|
614
|
+
} catch {
|
|
615
|
+
throw new Error(`Telegram poll returned invalid JSON: ${text}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!response.ok || payload?.ok === false) {
|
|
619
|
+
throw new Error(`Telegram poll failed with HTTP ${response.status}: ${text}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (!Array.isArray(payload?.result)) {
|
|
623
|
+
throw new Error("Telegram poll response did not include a result array.");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return payload.result;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function parseTelegramApprovalUpdate(update) {
|
|
630
|
+
const message = update.message ?? update.edited_message ?? update.channel_post;
|
|
631
|
+
const text = message?.text;
|
|
632
|
+
|
|
633
|
+
if (!text) {
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const match = text.trim().match(/^\/?(approve|approved|deny|denied)\s+([A-Za-z0-9_.:@-]+)(?:\s+(.+))?$/i);
|
|
638
|
+
|
|
639
|
+
if (!match) {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const decision = normalizeApprovalDecision(match[1].toLowerCase());
|
|
644
|
+
const actorName = message.from?.username ?? message.from?.id ?? message.chat?.username ?? message.chat?.id ?? "telegram-user";
|
|
645
|
+
const reason = match[3]?.trim();
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
updateId: update.update_id,
|
|
649
|
+
approvalId: match[2],
|
|
650
|
+
decision,
|
|
651
|
+
actor: `telegram:${actorName}`,
|
|
652
|
+
reason
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function nextTelegramOffset(currentOffset, updateId) {
|
|
657
|
+
if (!Number.isSafeInteger(updateId)) {
|
|
658
|
+
return currentOffset;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return Math.max(currentOffset ?? 0, updateId + 1);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function createApprovalDecision(approval, options) {
|
|
665
|
+
const decision = normalizeApprovalDecision(options.decision);
|
|
666
|
+
const status = decision === "approve" ? "approved" : "denied";
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
schemaVersion: "clawguard.decision.v1",
|
|
670
|
+
id: randomUUID(),
|
|
671
|
+
approvalId: approval.id,
|
|
672
|
+
status,
|
|
673
|
+
decision,
|
|
674
|
+
decidedAt: new Date().toISOString(),
|
|
675
|
+
actor: options.actor,
|
|
676
|
+
reason: options.reason,
|
|
677
|
+
framework: approval.framework,
|
|
678
|
+
target: approval.target,
|
|
679
|
+
destination: approval.destination,
|
|
680
|
+
risk: approval.risk,
|
|
681
|
+
policy: approval.policy,
|
|
682
|
+
source: {
|
|
683
|
+
path: path.resolve(options.approvalPath),
|
|
684
|
+
approvalCreatedAt: approval.createdAt
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
435
689
|
async function sendTelegramApproval(approval, message, options) {
|
|
436
690
|
const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
437
691
|
|
|
@@ -533,6 +787,36 @@ function printApprovalWatchResult(result) {
|
|
|
533
787
|
console.log(`Skipped: ${result.skipped}`);
|
|
534
788
|
}
|
|
535
789
|
|
|
790
|
+
function printApprovalDecisionResult(result) {
|
|
791
|
+
console.log(`ClawGuard approval decision: ${result.approval.id}`);
|
|
792
|
+
console.log(`Decision: ${formatDecision(result.decision.decision)}`);
|
|
793
|
+
console.log(`Status: ${result.decision.status}`);
|
|
794
|
+
console.log(`Actor: ${result.decision.actor}`);
|
|
795
|
+
if (result.decision.reason) {
|
|
796
|
+
console.log(`Reason: ${result.decision.reason}`);
|
|
797
|
+
}
|
|
798
|
+
console.log(`Output: ${result.outputPath}`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function printApprovalTelegramPollResult(result) {
|
|
802
|
+
console.log(`ClawGuard Telegram approval poll: ${result.approvalPath}`);
|
|
803
|
+
console.log(`Decisions: ${result.decisionsPath}`);
|
|
804
|
+
console.log(`Offset state: ${result.offsetStatePath}`);
|
|
805
|
+
console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
|
|
806
|
+
console.log(`Updates checked: ${result.checked}`);
|
|
807
|
+
console.log(`Commands found: ${result.commands}`);
|
|
808
|
+
console.log(`Decisions written: ${result.decided}`);
|
|
809
|
+
console.log(`Skipped: ${result.skipped}`);
|
|
810
|
+
console.log(`Next offset: ${result.nextOffset ?? "none"}`);
|
|
811
|
+
|
|
812
|
+
if (result.errors.length > 0) {
|
|
813
|
+
console.log("Errors:");
|
|
814
|
+
for (const error of result.errors) {
|
|
815
|
+
console.log(`- update ${error.updateId}: ${error.message}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
536
820
|
async function readApprovalRequest(approvalPath, id) {
|
|
537
821
|
const resolvedPath = path.resolve(approvalPath);
|
|
538
822
|
const approvals = await readApprovalRequests(resolvedPath);
|
|
@@ -609,6 +893,29 @@ async function writeApprovalWatchState(statePath, sentIds) {
|
|
|
609
893
|
}, null, 2)}\n`);
|
|
610
894
|
}
|
|
611
895
|
|
|
896
|
+
async function readTelegramOffsetState(statePath) {
|
|
897
|
+
try {
|
|
898
|
+
const content = await fs.readFile(statePath, "utf8");
|
|
899
|
+
const state = JSON.parse(content);
|
|
900
|
+
return Number.isSafeInteger(state.nextOffset) ? state.nextOffset : undefined;
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (error.code === "ENOENT") {
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function writeTelegramOffsetState(statePath, nextOffset) {
|
|
911
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
912
|
+
await fs.writeFile(statePath, `${JSON.stringify({
|
|
913
|
+
schemaVersion: "clawguard.telegram-offset.v1",
|
|
914
|
+
updatedAt: new Date().toISOString(),
|
|
915
|
+
nextOffset
|
|
916
|
+
}, null, 2)}\n`);
|
|
917
|
+
}
|
|
918
|
+
|
|
612
919
|
function printGateResult(result, options) {
|
|
613
920
|
const decision = result.policy.decision;
|
|
614
921
|
console.log(`ClawGuard gate: ${result.target}`);
|
|
@@ -931,6 +1238,14 @@ function commandLabel(commandName) {
|
|
|
931
1238
|
return "Approval watch";
|
|
932
1239
|
}
|
|
933
1240
|
|
|
1241
|
+
if (commandName === "approvals-decide") {
|
|
1242
|
+
return "Approval decision";
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (commandName === "approvals-poll-telegram") {
|
|
1246
|
+
return "Telegram approval poll";
|
|
1247
|
+
}
|
|
1248
|
+
|
|
934
1249
|
if (commandName === "gate") {
|
|
935
1250
|
return "Gate";
|
|
936
1251
|
}
|
|
@@ -977,6 +1292,18 @@ function sleep(ms) {
|
|
|
977
1292
|
});
|
|
978
1293
|
}
|
|
979
1294
|
|
|
1295
|
+
function normalizeApprovalDecision(value) {
|
|
1296
|
+
if (value === "approved") {
|
|
1297
|
+
return "approve";
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (value === "denied") {
|
|
1301
|
+
return "deny";
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return value;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
980
1307
|
function gateExitCode(decision) {
|
|
981
1308
|
if (decision === "allow") {
|
|
982
1309
|
return 0;
|
|
@@ -1393,6 +1720,179 @@ function parseApprovalWatchOptions(values) {
|
|
|
1393
1720
|
return options;
|
|
1394
1721
|
}
|
|
1395
1722
|
|
|
1723
|
+
function parseApprovalDecisionOptions(values) {
|
|
1724
|
+
const options = {
|
|
1725
|
+
approvalPath: undefined,
|
|
1726
|
+
id: undefined,
|
|
1727
|
+
decision: undefined,
|
|
1728
|
+
outPath: undefined,
|
|
1729
|
+
actor: "local-user",
|
|
1730
|
+
reason: undefined,
|
|
1731
|
+
json: false
|
|
1732
|
+
};
|
|
1733
|
+
const paths = [];
|
|
1734
|
+
|
|
1735
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1736
|
+
const value = values[index];
|
|
1737
|
+
|
|
1738
|
+
if (value === "--json") {
|
|
1739
|
+
options.json = true;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
if (value === "--id") {
|
|
1744
|
+
options.id = requireNextValue(values, index, "--id");
|
|
1745
|
+
index += 1;
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
if (value === "--decision") {
|
|
1750
|
+
options.decision = requireNextValue(values, index, "--decision");
|
|
1751
|
+
index += 1;
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (value === "--out") {
|
|
1756
|
+
options.outPath = requireNextValue(values, index, "--out");
|
|
1757
|
+
index += 1;
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
if (value === "--actor") {
|
|
1762
|
+
options.actor = requireNextValue(values, index, "--actor");
|
|
1763
|
+
index += 1;
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (value === "--reason") {
|
|
1768
|
+
options.reason = requireNextValue(values, index, "--reason");
|
|
1769
|
+
index += 1;
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
if (value.startsWith("--")) {
|
|
1774
|
+
throw new Error(`Unknown option: ${value}`);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
paths.push(value);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
options.approvalPath = paths[0];
|
|
1781
|
+
|
|
1782
|
+
if (!options.approvalPath) {
|
|
1783
|
+
throw new Error("approvals decide requires <approval.json|approvals.jsonl>.");
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
if (!options.id) {
|
|
1787
|
+
throw new Error("approvals decide requires --id <id>.");
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
if (!options.decision) {
|
|
1791
|
+
throw new Error("approvals decide requires --decision approve|deny.");
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
options.decision = normalizeApprovalDecision(options.decision);
|
|
1795
|
+
|
|
1796
|
+
if (!["approve", "deny"].includes(options.decision)) {
|
|
1797
|
+
throw new Error("Invalid --decision value. Use one of: approve, deny");
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
return options;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function parseApprovalTelegramPollOptions(values) {
|
|
1804
|
+
const options = {
|
|
1805
|
+
approvalPath: undefined,
|
|
1806
|
+
decisionsPath: undefined,
|
|
1807
|
+
offsetStatePath: undefined,
|
|
1808
|
+
botToken: undefined,
|
|
1809
|
+
telegramApiBase: undefined,
|
|
1810
|
+
telegramUpdatesPath: undefined,
|
|
1811
|
+
timeoutSeconds: 0,
|
|
1812
|
+
dryRun: false,
|
|
1813
|
+
json: false
|
|
1814
|
+
};
|
|
1815
|
+
const paths = [];
|
|
1816
|
+
|
|
1817
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1818
|
+
const value = values[index];
|
|
1819
|
+
|
|
1820
|
+
if (value === "--json") {
|
|
1821
|
+
options.json = true;
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
if (value === "--dry-run") {
|
|
1826
|
+
options.dryRun = true;
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
if (value === "--decisions") {
|
|
1831
|
+
options.decisionsPath = requireNextValue(values, index, "--decisions");
|
|
1832
|
+
index += 1;
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
if (value === "--out") {
|
|
1837
|
+
options.decisionsPath = requireNextValue(values, index, "--out");
|
|
1838
|
+
index += 1;
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
if (value === "--offset-state") {
|
|
1843
|
+
options.offsetStatePath = requireNextValue(values, index, "--offset-state");
|
|
1844
|
+
index += 1;
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
if (value === "--bot-token") {
|
|
1849
|
+
options.botToken = requireNextValue(values, index, "--bot-token");
|
|
1850
|
+
index += 1;
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
if (value === "--telegram-api-base") {
|
|
1855
|
+
options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
|
|
1856
|
+
index += 1;
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if (value === "--telegram-updates-file") {
|
|
1861
|
+
options.telegramUpdatesPath = requireNextValue(values, index, "--telegram-updates-file");
|
|
1862
|
+
index += 1;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
if (value === "--timeout") {
|
|
1867
|
+
const timeout = Number.parseInt(requireNextValue(values, index, "--timeout"), 10);
|
|
1868
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0) {
|
|
1869
|
+
throw new Error("--timeout must be a non-negative integer.");
|
|
1870
|
+
}
|
|
1871
|
+
options.timeoutSeconds = timeout;
|
|
1872
|
+
index += 1;
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
if (value.startsWith("--")) {
|
|
1877
|
+
throw new Error(`Unknown option: ${value}`);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
paths.push(value);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
options.approvalPath = paths[0];
|
|
1884
|
+
|
|
1885
|
+
if (!options.approvalPath) {
|
|
1886
|
+
throw new Error("approvals poll-telegram requires <approval.json|approvals.jsonl>.");
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
if (!options.decisionsPath) {
|
|
1890
|
+
throw new Error("approvals poll-telegram requires --decisions <path>.");
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
return options;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1396
1896
|
async function writeReportFile(outputPath, content) {
|
|
1397
1897
|
const resolvedPath = path.resolve(outputPath);
|
|
1398
1898
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|