@denial-web/clawguard 0.1.9 → 0.1.11
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 +23 -0
- package/docs/INTEGRATION_SPEC.md +36 -0
- package/package.json +1 -1
- package/src/cli.js +517 -3
package/README.md
CHANGED
|
@@ -99,6 +99,29 @@ npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <ap
|
|
|
99
99
|
npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <approval-id> --decision deny --reason "Unexpected shell access" --out ./.clawguard/decisions.jsonl
|
|
100
100
|
```
|
|
101
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
|
+
|
|
117
|
+
Apply the recorded decision to continue or block the pending install:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <approval-id> --decisions ./.clawguard/decisions.jsonl
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If the latest decision is `approve`, ClawGuard copies the original scanned source to the original approved destination. If the latest decision is `deny`, it exits blocked without copying. If no decision exists yet, it stays paused.
|
|
124
|
+
|
|
102
125
|
When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
|
|
103
126
|
|
|
104
127
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -93,6 +93,42 @@ clawguard approvals decide ./.clawguard/approvals.jsonl \
|
|
|
93
93
|
|
|
94
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
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
|
+
|
|
114
|
+
### Approval Apply
|
|
115
|
+
|
|
116
|
+
After a decision is recorded, ClawGuard can apply that decision to the original pending install:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
clawguard approvals apply ./.clawguard/approvals.jsonl \
|
|
120
|
+
--id <approval-id> \
|
|
121
|
+
--decisions ./.clawguard/decisions.jsonl
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Apply behavior:
|
|
125
|
+
|
|
126
|
+
- If the latest matching decision is `approve`, copy the original scanned `target` to the original approval `destination`.
|
|
127
|
+
- If the latest matching decision is `deny`, exit blocked and copy nothing.
|
|
128
|
+
- If no matching decision exists, exit paused and copy nothing.
|
|
129
|
+
- Never execute the skill or install dependencies during apply.
|
|
130
|
+
- Refuse symlinked install sources and existing destinations, matching normal install safety.
|
|
131
|
+
|
|
96
132
|
### Skill Folder Scan
|
|
97
133
|
|
|
98
134
|
Command:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -39,7 +39,9 @@ if (![
|
|
|
39
39
|
"install",
|
|
40
40
|
"approvals-send",
|
|
41
41
|
"approvals-watch",
|
|
42
|
-
"approvals-decide"
|
|
42
|
+
"approvals-decide",
|
|
43
|
+
"approvals-poll-telegram",
|
|
44
|
+
"approvals-apply"
|
|
43
45
|
].includes(command)) {
|
|
44
46
|
console.error(`Unknown command: ${command}`);
|
|
45
47
|
printHelp();
|
|
@@ -82,6 +84,28 @@ try {
|
|
|
82
84
|
process.exit(0);
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
if (command === "approvals-poll-telegram") {
|
|
88
|
+
const pollOptions = parseApprovalTelegramPollOptions(optionValues);
|
|
89
|
+
const result = await pollTelegramApprovals(pollOptions);
|
|
90
|
+
if (pollOptions.json) {
|
|
91
|
+
console.log(JSON.stringify(result, null, 2));
|
|
92
|
+
} else {
|
|
93
|
+
printApprovalTelegramPollResult(result);
|
|
94
|
+
}
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (command === "approvals-apply") {
|
|
99
|
+
const applyOptions = parseApprovalApplyOptions(optionValues);
|
|
100
|
+
const result = await applyApprovalDecision(applyOptions);
|
|
101
|
+
if (applyOptions.json) {
|
|
102
|
+
console.log(JSON.stringify(result, null, 2));
|
|
103
|
+
} else {
|
|
104
|
+
printApprovalApplyResult(result);
|
|
105
|
+
}
|
|
106
|
+
process.exit(approvalApplyExitCode(result));
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
const cliOptions = parseOptions(optionValues);
|
|
86
110
|
cliOptions.framework = framework;
|
|
87
111
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -145,6 +169,8 @@ Usage:
|
|
|
145
169
|
clawguard approvals send <approval.json|approvals.jsonl> --via telegram --chat-id <id>
|
|
146
170
|
clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
|
|
147
171
|
clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
|
|
172
|
+
clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
|
|
173
|
+
clawguard approvals apply <approvals.jsonl> --id <id> --decisions <decisions.jsonl>
|
|
148
174
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
149
175
|
npm run scan -- <path>
|
|
150
176
|
|
|
@@ -182,8 +208,12 @@ Options:
|
|
|
182
208
|
--id <id> Approval id for send or decide.
|
|
183
209
|
--decision <value> Approval decision: approve, deny.
|
|
184
210
|
--out <path> Decision JSONL output file.
|
|
211
|
+
--decisions <path> Decision JSONL output file for reply polling.
|
|
185
212
|
--actor <name> Decision actor. Default: local-user.
|
|
186
213
|
--reason <text> Decision reason.
|
|
214
|
+
--offset-state <path> Telegram update offset state file.
|
|
215
|
+
--telegram-updates-file <path>
|
|
216
|
+
Read Telegram updates from a JSON file for tests or offline replay.
|
|
187
217
|
|
|
188
218
|
Gate exit codes:
|
|
189
219
|
0 = allow
|
|
@@ -200,6 +230,8 @@ Examples:
|
|
|
200
230
|
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
201
231
|
npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
202
232
|
npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
|
|
233
|
+
npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
|
|
234
|
+
npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <id> --decisions ./.clawguard/decisions.jsonl
|
|
203
235
|
npm run scan -- examples/risky-skill
|
|
204
236
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
205
237
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -307,6 +339,22 @@ function parseCommand(values) {
|
|
|
307
339
|
};
|
|
308
340
|
}
|
|
309
341
|
|
|
342
|
+
if (rawCommand === "approvals" && values[1] === "poll-telegram") {
|
|
343
|
+
return {
|
|
344
|
+
command: "approvals-poll-telegram",
|
|
345
|
+
framework: undefined,
|
|
346
|
+
optionValues: values.slice(2)
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (rawCommand === "approvals" && values[1] === "apply") {
|
|
351
|
+
return {
|
|
352
|
+
command: "approvals-apply",
|
|
353
|
+
framework: undefined,
|
|
354
|
+
optionValues: values.slice(2)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
310
358
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
311
359
|
const nestedCommand = values[1];
|
|
312
360
|
|
|
@@ -471,8 +519,7 @@ async function decideApproval(options) {
|
|
|
471
519
|
const outputPath = path.resolve(options.outPath ?? `${options.approvalPath}.decisions.jsonl`);
|
|
472
520
|
const decision = createApprovalDecision(approval, options);
|
|
473
521
|
|
|
474
|
-
await
|
|
475
|
-
await fs.appendFile(outputPath, `${JSON.stringify(decision)}\n`);
|
|
522
|
+
await appendApprovalDecision(outputPath, decision);
|
|
476
523
|
|
|
477
524
|
return {
|
|
478
525
|
approval: {
|
|
@@ -487,6 +534,246 @@ async function decideApproval(options) {
|
|
|
487
534
|
};
|
|
488
535
|
}
|
|
489
536
|
|
|
537
|
+
async function appendApprovalDecision(outputPath, decision) {
|
|
538
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
539
|
+
await fs.appendFile(outputPath, `${JSON.stringify(decision)}\n`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function pollTelegramApprovals(options) {
|
|
543
|
+
const offsetStatePath = path.resolve(options.offsetStatePath ?? `${options.decisionsPath}.telegram-state.json`);
|
|
544
|
+
const offset = await readTelegramOffsetState(offsetStatePath);
|
|
545
|
+
const updates = await fetchTelegramUpdates(options, offset);
|
|
546
|
+
const outputPath = path.resolve(options.decisionsPath);
|
|
547
|
+
const result = {
|
|
548
|
+
approvalPath: path.resolve(options.approvalPath),
|
|
549
|
+
decisionsPath: outputPath,
|
|
550
|
+
offsetStatePath,
|
|
551
|
+
dryRun: options.dryRun,
|
|
552
|
+
checked: updates.length,
|
|
553
|
+
commands: 0,
|
|
554
|
+
decided: 0,
|
|
555
|
+
skipped: 0,
|
|
556
|
+
errors: [],
|
|
557
|
+
decisions: [],
|
|
558
|
+
nextOffset: offset
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
for (const update of updates) {
|
|
562
|
+
const parsed = parseTelegramApprovalUpdate(update);
|
|
563
|
+
|
|
564
|
+
if (!parsed) {
|
|
565
|
+
result.skipped += 1;
|
|
566
|
+
result.nextOffset = nextTelegramOffset(result.nextOffset, update.update_id);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
result.commands += 1;
|
|
571
|
+
result.nextOffset = nextTelegramOffset(result.nextOffset, update.update_id);
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const approval = await readApprovalRequest(options.approvalPath, parsed.approvalId);
|
|
575
|
+
const decision = createApprovalDecision(approval, {
|
|
576
|
+
approvalPath: options.approvalPath,
|
|
577
|
+
decision: parsed.decision,
|
|
578
|
+
actor: parsed.actor,
|
|
579
|
+
reason: parsed.reason
|
|
580
|
+
});
|
|
581
|
+
result.decisions.push(decision);
|
|
582
|
+
|
|
583
|
+
if (!options.dryRun) {
|
|
584
|
+
await appendApprovalDecision(outputPath, decision);
|
|
585
|
+
result.decided += 1;
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
result.errors.push({
|
|
589
|
+
updateId: update.update_id,
|
|
590
|
+
approvalId: parsed.approvalId,
|
|
591
|
+
message: error.message
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!options.dryRun && result.nextOffset !== offset) {
|
|
597
|
+
await writeTelegramOffsetState(offsetStatePath, result.nextOffset);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function applyApprovalDecision(options) {
|
|
604
|
+
const approval = await readApprovalRequest(options.approvalPath, options.id);
|
|
605
|
+
const decisionsPath = path.resolve(options.decisionsPath ?? `${options.approvalPath}.decisions.jsonl`);
|
|
606
|
+
const decision = await readLatestApprovalDecision(decisionsPath, approval.id);
|
|
607
|
+
const result = {
|
|
608
|
+
approval: {
|
|
609
|
+
id: approval.id,
|
|
610
|
+
status: approval.status,
|
|
611
|
+
decision: approval.decision,
|
|
612
|
+
risk: approval.risk,
|
|
613
|
+
framework: approval.framework
|
|
614
|
+
},
|
|
615
|
+
decision,
|
|
616
|
+
decisionsPath,
|
|
617
|
+
source: approval.target ? path.resolve(approval.target) : undefined,
|
|
618
|
+
destination: approval.destination ? path.resolve(approval.destination) : undefined,
|
|
619
|
+
dryRun: options.dryRun,
|
|
620
|
+
installed: false,
|
|
621
|
+
skipped: true,
|
|
622
|
+
reason: undefined
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
if (!decision) {
|
|
626
|
+
result.reason = "No decision has been recorded for this approval.";
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (decision.decision !== "approve") {
|
|
631
|
+
result.reason = decision.reason ?? "Approval was denied.";
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!result.source) {
|
|
636
|
+
throw new Error("Approval request has no target path to install.");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (!result.destination) {
|
|
640
|
+
throw new Error("Approval request has no destination path to install.");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (options.dryRun) {
|
|
644
|
+
result.skipped = false;
|
|
645
|
+
result.reason = "Dry run passed; no files were copied.";
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
await assertInstallableSource(result.source);
|
|
650
|
+
await assertDestinationAvailable(result.destination);
|
|
651
|
+
await fs.mkdir(path.dirname(result.destination), { recursive: true });
|
|
652
|
+
await fs.cp(result.source, result.destination, {
|
|
653
|
+
recursive: true,
|
|
654
|
+
errorOnExist: true,
|
|
655
|
+
force: false,
|
|
656
|
+
verbatimSymlinks: true
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
result.installed = true;
|
|
660
|
+
result.skipped = false;
|
|
661
|
+
result.reason = "Copied after recorded approval.";
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function readLatestApprovalDecision(decisionsPath, approvalId) {
|
|
666
|
+
let decisions;
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
decisions = await readApprovalDecisions(decisionsPath);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
if (error.code === "ENOENT") {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return decisions.filter((decision) => decision.approvalId === approvalId).at(-1);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function readApprovalDecisions(decisionsPath) {
|
|
682
|
+
const content = await fs.readFile(decisionsPath, "utf8");
|
|
683
|
+
const decisions = content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
684
|
+
|
|
685
|
+
for (const decision of decisions) {
|
|
686
|
+
if (decision.schemaVersion !== "clawguard.decision.v1") {
|
|
687
|
+
throw new Error("Unsupported approval decision schema.");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return decisions;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function fetchTelegramUpdates(options, offset) {
|
|
695
|
+
if (options.telegramUpdatesPath) {
|
|
696
|
+
const content = await fs.readFile(path.resolve(options.telegramUpdatesPath), "utf8");
|
|
697
|
+
const payload = JSON.parse(content);
|
|
698
|
+
const updates = Array.isArray(payload) ? payload : payload.result;
|
|
699
|
+
|
|
700
|
+
if (!Array.isArray(updates)) {
|
|
701
|
+
throw new Error("Telegram updates file must be an array or an object with a result array.");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return updates.filter((update) => !offset || !Number.isSafeInteger(update.update_id) || update.update_id >= offset);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
708
|
+
|
|
709
|
+
if (!botToken) {
|
|
710
|
+
throw new Error("Telegram poll requires --bot-token or TELEGRAM_BOT_TOKEN.");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const apiBase = options.telegramApiBase ?? "https://api.telegram.org";
|
|
714
|
+
const endpoint = new URL(`${apiBase.replace(/\/$/, "")}/bot${botToken}/getUpdates`);
|
|
715
|
+
endpoint.searchParams.set("timeout", String(options.timeoutSeconds));
|
|
716
|
+
|
|
717
|
+
if (offset) {
|
|
718
|
+
endpoint.searchParams.set("offset", String(offset));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const response = await fetch(endpoint);
|
|
722
|
+
const text = await response.text();
|
|
723
|
+
let payload;
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
payload = text ? JSON.parse(text) : null;
|
|
727
|
+
} catch {
|
|
728
|
+
throw new Error(`Telegram poll returned invalid JSON: ${text}`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (!response.ok || payload?.ok === false) {
|
|
732
|
+
throw new Error(`Telegram poll failed with HTTP ${response.status}: ${text}`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (!Array.isArray(payload?.result)) {
|
|
736
|
+
throw new Error("Telegram poll response did not include a result array.");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return payload.result;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function parseTelegramApprovalUpdate(update) {
|
|
743
|
+
const message = update.message ?? update.edited_message ?? update.channel_post;
|
|
744
|
+
const text = message?.text;
|
|
745
|
+
|
|
746
|
+
if (!text) {
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const match = text.trim().match(/^\/?(approve|approved|deny|denied)\s+([A-Za-z0-9_.:@-]+)(?:\s+(.+))?$/i);
|
|
751
|
+
|
|
752
|
+
if (!match) {
|
|
753
|
+
return undefined;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const decision = normalizeApprovalDecision(match[1].toLowerCase());
|
|
757
|
+
const actorName = message.from?.username ?? message.from?.id ?? message.chat?.username ?? message.chat?.id ?? "telegram-user";
|
|
758
|
+
const reason = match[3]?.trim();
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
updateId: update.update_id,
|
|
762
|
+
approvalId: match[2],
|
|
763
|
+
decision,
|
|
764
|
+
actor: `telegram:${actorName}`,
|
|
765
|
+
reason
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function nextTelegramOffset(currentOffset, updateId) {
|
|
770
|
+
if (!Number.isSafeInteger(updateId)) {
|
|
771
|
+
return currentOffset;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return Math.max(currentOffset ?? 0, updateId + 1);
|
|
775
|
+
}
|
|
776
|
+
|
|
490
777
|
function createApprovalDecision(approval, options) {
|
|
491
778
|
const decision = normalizeApprovalDecision(options.decision);
|
|
492
779
|
const status = decision === "approve" ? "approved" : "denied";
|
|
@@ -624,6 +911,36 @@ function printApprovalDecisionResult(result) {
|
|
|
624
911
|
console.log(`Output: ${result.outputPath}`);
|
|
625
912
|
}
|
|
626
913
|
|
|
914
|
+
function printApprovalTelegramPollResult(result) {
|
|
915
|
+
console.log(`ClawGuard Telegram approval poll: ${result.approvalPath}`);
|
|
916
|
+
console.log(`Decisions: ${result.decisionsPath}`);
|
|
917
|
+
console.log(`Offset state: ${result.offsetStatePath}`);
|
|
918
|
+
console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
|
|
919
|
+
console.log(`Updates checked: ${result.checked}`);
|
|
920
|
+
console.log(`Commands found: ${result.commands}`);
|
|
921
|
+
console.log(`Decisions written: ${result.decided}`);
|
|
922
|
+
console.log(`Skipped: ${result.skipped}`);
|
|
923
|
+
console.log(`Next offset: ${result.nextOffset ?? "none"}`);
|
|
924
|
+
|
|
925
|
+
if (result.errors.length > 0) {
|
|
926
|
+
console.log("Errors:");
|
|
927
|
+
for (const error of result.errors) {
|
|
928
|
+
console.log(`- update ${error.updateId}: ${error.message}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function printApprovalApplyResult(result) {
|
|
934
|
+
console.log(`ClawGuard approval apply: ${result.approval.id}`);
|
|
935
|
+
console.log(`Decision: ${result.decision ? formatDecision(result.decision.decision) : "PENDING"}`);
|
|
936
|
+
console.log(`Source: ${result.source ?? "not recorded"}`);
|
|
937
|
+
console.log(`Destination: ${result.destination ?? "not recorded"}`);
|
|
938
|
+
console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
|
|
939
|
+
console.log(`Installed: ${result.installed ? "yes" : "no"}`);
|
|
940
|
+
console.log(`Exit code: ${approvalApplyExitCode(result)}`);
|
|
941
|
+
console.log(`Reason: ${result.reason}`);
|
|
942
|
+
}
|
|
943
|
+
|
|
627
944
|
async function readApprovalRequest(approvalPath, id) {
|
|
628
945
|
const resolvedPath = path.resolve(approvalPath);
|
|
629
946
|
const approvals = await readApprovalRequests(resolvedPath);
|
|
@@ -700,6 +1017,29 @@ async function writeApprovalWatchState(statePath, sentIds) {
|
|
|
700
1017
|
}, null, 2)}\n`);
|
|
701
1018
|
}
|
|
702
1019
|
|
|
1020
|
+
async function readTelegramOffsetState(statePath) {
|
|
1021
|
+
try {
|
|
1022
|
+
const content = await fs.readFile(statePath, "utf8");
|
|
1023
|
+
const state = JSON.parse(content);
|
|
1024
|
+
return Number.isSafeInteger(state.nextOffset) ? state.nextOffset : undefined;
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
if (error.code === "ENOENT") {
|
|
1027
|
+
return undefined;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
throw error;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async function writeTelegramOffsetState(statePath, nextOffset) {
|
|
1035
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1036
|
+
await fs.writeFile(statePath, `${JSON.stringify({
|
|
1037
|
+
schemaVersion: "clawguard.telegram-offset.v1",
|
|
1038
|
+
updatedAt: new Date().toISOString(),
|
|
1039
|
+
nextOffset
|
|
1040
|
+
}, null, 2)}\n`);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
703
1043
|
function printGateResult(result, options) {
|
|
704
1044
|
const decision = result.policy.decision;
|
|
705
1045
|
console.log(`ClawGuard gate: ${result.target}`);
|
|
@@ -1026,6 +1366,14 @@ function commandLabel(commandName) {
|
|
|
1026
1366
|
return "Approval decision";
|
|
1027
1367
|
}
|
|
1028
1368
|
|
|
1369
|
+
if (commandName === "approvals-poll-telegram") {
|
|
1370
|
+
return "Telegram approval poll";
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (commandName === "approvals-apply") {
|
|
1374
|
+
return "Approval apply";
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1029
1377
|
if (commandName === "gate") {
|
|
1030
1378
|
return "Gate";
|
|
1031
1379
|
}
|
|
@@ -1104,6 +1452,18 @@ function installExitCode(decision, install) {
|
|
|
1104
1452
|
return gateExitCode(decision);
|
|
1105
1453
|
}
|
|
1106
1454
|
|
|
1455
|
+
function approvalApplyExitCode(result) {
|
|
1456
|
+
if (!result.decision) {
|
|
1457
|
+
return 1;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (result.decision.decision !== "approve") {
|
|
1461
|
+
return 2;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return 0;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1107
1467
|
function parseOptions(values) {
|
|
1108
1468
|
const options = {
|
|
1109
1469
|
json: false,
|
|
@@ -1580,6 +1940,160 @@ function parseApprovalDecisionOptions(values) {
|
|
|
1580
1940
|
return options;
|
|
1581
1941
|
}
|
|
1582
1942
|
|
|
1943
|
+
function parseApprovalTelegramPollOptions(values) {
|
|
1944
|
+
const options = {
|
|
1945
|
+
approvalPath: undefined,
|
|
1946
|
+
decisionsPath: undefined,
|
|
1947
|
+
offsetStatePath: undefined,
|
|
1948
|
+
botToken: undefined,
|
|
1949
|
+
telegramApiBase: undefined,
|
|
1950
|
+
telegramUpdatesPath: undefined,
|
|
1951
|
+
timeoutSeconds: 0,
|
|
1952
|
+
dryRun: false,
|
|
1953
|
+
json: false
|
|
1954
|
+
};
|
|
1955
|
+
const paths = [];
|
|
1956
|
+
|
|
1957
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1958
|
+
const value = values[index];
|
|
1959
|
+
|
|
1960
|
+
if (value === "--json") {
|
|
1961
|
+
options.json = true;
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
if (value === "--dry-run") {
|
|
1966
|
+
options.dryRun = true;
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
if (value === "--decisions") {
|
|
1971
|
+
options.decisionsPath = requireNextValue(values, index, "--decisions");
|
|
1972
|
+
index += 1;
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (value === "--out") {
|
|
1977
|
+
options.decisionsPath = requireNextValue(values, index, "--out");
|
|
1978
|
+
index += 1;
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
if (value === "--offset-state") {
|
|
1983
|
+
options.offsetStatePath = requireNextValue(values, index, "--offset-state");
|
|
1984
|
+
index += 1;
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
if (value === "--bot-token") {
|
|
1989
|
+
options.botToken = requireNextValue(values, index, "--bot-token");
|
|
1990
|
+
index += 1;
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
if (value === "--telegram-api-base") {
|
|
1995
|
+
options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
|
|
1996
|
+
index += 1;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
if (value === "--telegram-updates-file") {
|
|
2001
|
+
options.telegramUpdatesPath = requireNextValue(values, index, "--telegram-updates-file");
|
|
2002
|
+
index += 1;
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
if (value === "--timeout") {
|
|
2007
|
+
const timeout = Number.parseInt(requireNextValue(values, index, "--timeout"), 10);
|
|
2008
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0) {
|
|
2009
|
+
throw new Error("--timeout must be a non-negative integer.");
|
|
2010
|
+
}
|
|
2011
|
+
options.timeoutSeconds = timeout;
|
|
2012
|
+
index += 1;
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
if (value.startsWith("--")) {
|
|
2017
|
+
throw new Error(`Unknown option: ${value}`);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
paths.push(value);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
options.approvalPath = paths[0];
|
|
2024
|
+
|
|
2025
|
+
if (!options.approvalPath) {
|
|
2026
|
+
throw new Error("approvals poll-telegram requires <approval.json|approvals.jsonl>.");
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
if (!options.decisionsPath) {
|
|
2030
|
+
throw new Error("approvals poll-telegram requires --decisions <path>.");
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
return options;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function parseApprovalApplyOptions(values) {
|
|
2037
|
+
const options = {
|
|
2038
|
+
approvalPath: undefined,
|
|
2039
|
+
id: undefined,
|
|
2040
|
+
decisionsPath: undefined,
|
|
2041
|
+
dryRun: false,
|
|
2042
|
+
json: false
|
|
2043
|
+
};
|
|
2044
|
+
const paths = [];
|
|
2045
|
+
|
|
2046
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
2047
|
+
const value = values[index];
|
|
2048
|
+
|
|
2049
|
+
if (value === "--json") {
|
|
2050
|
+
options.json = true;
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
if (value === "--dry-run") {
|
|
2055
|
+
options.dryRun = true;
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
if (value === "--id") {
|
|
2060
|
+
options.id = requireNextValue(values, index, "--id");
|
|
2061
|
+
index += 1;
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if (value === "--decisions") {
|
|
2066
|
+
options.decisionsPath = requireNextValue(values, index, "--decisions");
|
|
2067
|
+
index += 1;
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (value === "--out") {
|
|
2072
|
+
options.decisionsPath = requireNextValue(values, index, "--out");
|
|
2073
|
+
index += 1;
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
if (value.startsWith("--")) {
|
|
2078
|
+
throw new Error(`Unknown option: ${value}`);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
paths.push(value);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
options.approvalPath = paths[0];
|
|
2085
|
+
|
|
2086
|
+
if (!options.approvalPath) {
|
|
2087
|
+
throw new Error("approvals apply requires <approval.json|approvals.jsonl>.");
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
if (!options.id) {
|
|
2091
|
+
throw new Error("approvals apply requires --id <id>.");
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
return options;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1583
2097
|
async function writeReportFile(outputPath, content) {
|
|
1584
2098
|
const resolvedPath = path.resolve(outputPath);
|
|
1585
2099
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|