agentcollar 0.0.1 → 0.1.0
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/LICENSE +27 -0
- package/README.md +82 -4
- package/dist/approval.js +67 -0
- package/dist/audit-log.js +37 -0
- package/dist/audit.js +23 -0
- package/dist/check.js +52 -0
- package/dist/cli/format.js +39 -0
- package/dist/cli/intro-frames.js +25 -0
- package/dist/cli/intro.js +105 -0
- package/dist/cli/logs.js +46 -0
- package/dist/cli/main.js +94 -0
- package/dist/cli/mandates.js +76 -0
- package/dist/cli/revoke.js +8 -0
- package/dist/cli/setup.js +124 -0
- package/dist/cli/start.js +29 -0
- package/dist/cli/tail.js +46 -0
- package/dist/cli/watch.js +41 -0
- package/dist/cli.js +4 -0
- package/dist/env.js +13 -0
- package/dist/gmail-fake.js +27 -0
- package/dist/mandate.js +82 -0
- package/dist/mcp/protocol.js +72 -0
- package/dist/mcp/server.js +22 -0
- package/dist/mcp/tools.js +112 -0
- package/dist/paths.js +42 -0
- package/dist/revoke.js +18 -0
- package/dist/server.js +238 -0
- package/dist/setup.js +100 -0
- package/dist/snapshot.js +42 -0
- package/dist/telegram.js +124 -0
- package/package.json +50 -5
package/dist/telegram.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Telegram channel for human approval, using the Bot API directly with fetch.
|
|
2
|
+
// Long polling: we ask Telegram "anything new?" and it holds the request open
|
|
3
|
+
// up to 30 seconds until something happens. No public server or webhook needed.
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
import { decide, describe } from "./approval.js";
|
|
6
|
+
// One Bot API call. The bot token is part of the URL, so we never print the URL.
|
|
7
|
+
async function callApi(config, method, params) {
|
|
8
|
+
const response = await fetch(`https://api.telegram.org/bot${config.botToken}/${method}`, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "content-type": "application/json" },
|
|
11
|
+
body: JSON.stringify(params),
|
|
12
|
+
});
|
|
13
|
+
const data = (await response.json());
|
|
14
|
+
if (!data.ok) {
|
|
15
|
+
throw new Error(`Telegram ${method} failed: ${data.description}`);
|
|
16
|
+
}
|
|
17
|
+
return data.result;
|
|
18
|
+
}
|
|
19
|
+
function buttons(mandate) {
|
|
20
|
+
if (mandate.status === "pending") {
|
|
21
|
+
return [[
|
|
22
|
+
{ text: "✅ Одобрить", callback_data: `approve:${mandate.id}` },
|
|
23
|
+
{ text: "❌ Отклонить", callback_data: `deny:${mandate.id}` },
|
|
24
|
+
]];
|
|
25
|
+
}
|
|
26
|
+
if (mandate.status === "approved" && !mandate.revoked) {
|
|
27
|
+
return [[{ text: "🛑 Отозвать", callback_data: `revoke:${mandate.id}` }]];
|
|
28
|
+
}
|
|
29
|
+
return []; // denied or revoked: nothing left to press
|
|
30
|
+
}
|
|
31
|
+
function statusLine(mandate) {
|
|
32
|
+
if (mandate.revoked)
|
|
33
|
+
return "🛑 Отозван";
|
|
34
|
+
if (mandate.status === "approved")
|
|
35
|
+
return "✅ Одобрен";
|
|
36
|
+
if (mandate.status === "denied")
|
|
37
|
+
return "❌ Отклонён";
|
|
38
|
+
return "⏳ Ждёт решения";
|
|
39
|
+
}
|
|
40
|
+
// Which Telegram message shows which mandate, so it can be edited later (e.g. on timeout).
|
|
41
|
+
const sentMessages = new Map(); // mandate id -> message
|
|
42
|
+
// Sent when an agent asks for a mandate. Only the public id goes to Telegram, never the token.
|
|
43
|
+
// Plain text (no parse_mode): the agent wrote the task text, so we do not let it format anything.
|
|
44
|
+
export async function sendApprovalRequest(config, mandate) {
|
|
45
|
+
const message = (await callApi(config, "sendMessage", {
|
|
46
|
+
chat_id: config.approverId, // in a private chat, chat id = user id
|
|
47
|
+
text: `🔐 Запрос мандата\n\n${describe(mandate)}\n\n${statusLine(mandate)}`,
|
|
48
|
+
reply_markup: { inline_keyboard: buttons(mandate) },
|
|
49
|
+
}));
|
|
50
|
+
sentMessages.set(mandate.id, { chatId: config.approverId, messageId: message.message_id });
|
|
51
|
+
}
|
|
52
|
+
// The mandate ended outside Telegram: the message must not keep showing live buttons.
|
|
53
|
+
async function closeMessage(config, mandate, finalLine) {
|
|
54
|
+
const sent = sentMessages.get(mandate.id);
|
|
55
|
+
if (sent === undefined)
|
|
56
|
+
return; // this mandate was never shown in Telegram
|
|
57
|
+
sentMessages.delete(mandate.id);
|
|
58
|
+
await callApi(config, "editMessageText", {
|
|
59
|
+
chat_id: sent.chatId,
|
|
60
|
+
message_id: sent.messageId,
|
|
61
|
+
text: `🔐 Запрос мандата\n\n${describe(mandate)}\n\n${finalLine}`,
|
|
62
|
+
reply_markup: { inline_keyboard: [] },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Nobody answered in time.
|
|
66
|
+
export async function notifyTimedOut(config, mandate) {
|
|
67
|
+
await closeMessage(config, mandate, "⌛ Время вышло");
|
|
68
|
+
}
|
|
69
|
+
// Revoked from the terminal (npm run revoke) or over HTTP.
|
|
70
|
+
export async function notifyRevoked(config, mandate) {
|
|
71
|
+
await closeMessage(config, mandate, "🛑 Отозван");
|
|
72
|
+
}
|
|
73
|
+
// A button was pressed.
|
|
74
|
+
async function handleButton(config, query) {
|
|
75
|
+
// Anyone can find the bot and press buttons in a forwarded message.
|
|
76
|
+
// Only the approver's presses count.
|
|
77
|
+
if (query.from.id !== config.approverId) {
|
|
78
|
+
console.log(`Telegram: ignored a button press from user ${query.from.id} (not the approver)`);
|
|
79
|
+
await callApi(config, "answerCallbackQuery", { callback_query_id: query.id, text: "Нет доступа" });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const [action, id] = (query.data ?? "").split(":");
|
|
83
|
+
const decision = action;
|
|
84
|
+
const mandate = ["approve", "deny", "revoke"].includes(action) && id ? decide(id, decision, "telegram") : undefined;
|
|
85
|
+
// Telegram shows a spinner on the button until we answer.
|
|
86
|
+
await callApi(config, "answerCallbackQuery", {
|
|
87
|
+
callback_query_id: query.id,
|
|
88
|
+
text: mandate ? statusLine(mandate) : "Уже решено или мандат не найден",
|
|
89
|
+
});
|
|
90
|
+
// Update the message: new status line, new buttons (e.g. "Отозвать" after approval).
|
|
91
|
+
if (mandate && query.message) {
|
|
92
|
+
await callApi(config, "editMessageText", {
|
|
93
|
+
chat_id: query.message.chat.id,
|
|
94
|
+
message_id: query.message.message_id,
|
|
95
|
+
text: `🔐 Запрос мандата\n\n${describe(mandate)}\n\n${statusLine(mandate)}`,
|
|
96
|
+
reply_markup: { inline_keyboard: buttons(mandate) },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Runs forever in the background of the server.
|
|
101
|
+
export async function startTelegramPolling(config) {
|
|
102
|
+
const me = (await callApi(config, "getMe", {}));
|
|
103
|
+
console.log(`Telegram: bot @${me.username} is listening, approver user id ${config.approverId}`);
|
|
104
|
+
let offset = 0; // "give me updates newer than this one"
|
|
105
|
+
while (true) {
|
|
106
|
+
try {
|
|
107
|
+
const updates = (await callApi(config, "getUpdates", {
|
|
108
|
+
offset,
|
|
109
|
+
timeout: 30, // long polling: wait up to 30 s for something new
|
|
110
|
+
allowed_updates: ["callback_query"],
|
|
111
|
+
}));
|
|
112
|
+
for (const update of updates) {
|
|
113
|
+
offset = update.update_id + 1; // mark as handled, even if handling fails
|
|
114
|
+
if (update.callback_query) {
|
|
115
|
+
await handleButton(config, update.callback_query);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error(`Telegram: ${error.message}, retrying in 5 s`);
|
|
121
|
+
await sleep(5000);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentcollar",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A small local broker between your AI agents and your accounts: short-lived, task-scoped mandates that you approve in Telegram, instead of passwords.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agents",
|
|
7
|
+
"mcp",
|
|
8
|
+
"security",
|
|
9
|
+
"gmail",
|
|
10
|
+
"telegram",
|
|
11
|
+
"claude-code",
|
|
12
|
+
"audit-log",
|
|
13
|
+
"least-privilege"
|
|
14
|
+
],
|
|
5
15
|
"homepage": "https://ank8dev.github.io/agentcollar/",
|
|
6
|
-
"repository": {
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/ank8dev/agentcollar.git",
|
|
19
|
+
"directory": "broker"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/ank8dev/agentcollar/issues"
|
|
23
|
+
},
|
|
7
24
|
"author": "ank8dev",
|
|
8
|
-
"license": "
|
|
9
|
-
"
|
|
25
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"agentcollar": "dist/cli.js",
|
|
29
|
+
"agcl": "dist/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"prepare": "npm run build",
|
|
42
|
+
"demo": "tsx src/demo.ts",
|
|
43
|
+
"server": "tsx src/server.ts",
|
|
44
|
+
"agent": "tsx src/agent.ts",
|
|
45
|
+
"setup": "tsx src/cli.ts setup",
|
|
46
|
+
"revoke": "tsx src/cli.ts revoke",
|
|
47
|
+
"test": "AGENTCOLLAR_HOME=${TMPDIR:-/tmp}/agentcollar-test-home tsx --test test/*.test.ts",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^26.6.4",
|
|
52
|
+
"tsx": "^4.23.15",
|
|
53
|
+
"typescript": "^7.0.2"
|
|
54
|
+
}
|
|
10
55
|
}
|