agentforge-telegram-gateway 3.1.0__py3-none-any.whl

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.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
agent_client.py ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """Small dependency-free client SDK/CLI for user-owned agents."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import base64
7
+ import json
8
+ import mimetypes
9
+ import urllib.error
10
+ import urllib.parse
11
+ import urllib.request
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ __all__ = ["TelegramAgentClient"]
16
+
17
+
18
+ class TelegramAgentClient:
19
+ def __init__(self, source: str, *, key: str = "", base_url: str = "http://127.0.0.1:8818", timeout: float = 15.0) -> None:
20
+ self.source = source
21
+ self.key = key.strip()
22
+ self.base_url = base_url.rstrip("/")
23
+ self.timeout = timeout
24
+
25
+ @classmethod
26
+ def from_key_file(cls, source: str, key_file: str | Path, **kwargs: Any) -> "TelegramAgentClient":
27
+ return cls(source, key=Path(key_file).read_text(encoding="utf-8").strip(), **kwargs)
28
+
29
+ def _request(self, method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
30
+ headers = {"X-Notifier-Source": self.source, "Accept": "application/json"}
31
+ if self.key:
32
+ headers["Authorization"] = f"Bearer {self.key}"
33
+ data = None
34
+ if payload is not None:
35
+ data = json.dumps(payload, separators=(",", ":")).encode("utf-8")
36
+ headers["Content-Type"] = "application/json"
37
+ request = urllib.request.Request(self.base_url + path, data=data, headers=headers, method=method)
38
+ try:
39
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
40
+ result = json.loads(response.read().decode("utf-8"))
41
+ except urllib.error.HTTPError as exc:
42
+ try:
43
+ result = json.loads(exc.read().decode("utf-8"))
44
+ error = str(result.get("error") or "request_failed")
45
+ except (ValueError, TypeError, UnicodeDecodeError):
46
+ error = "request_failed"
47
+ raise RuntimeError(error) from None
48
+ except (urllib.error.URLError, TimeoutError, OSError, ValueError):
49
+ raise RuntimeError("gateway_unavailable") from None
50
+ if not isinstance(result, dict) or not result.get("ok"):
51
+ raise RuntimeError(str(result.get("error") if isinstance(result, dict) else "invalid_gateway_response"))
52
+ return result
53
+
54
+ def inbox(self, *, after: str = "", limit: int = 50) -> dict[str, Any]:
55
+ query = urllib.parse.urlencode({"after": after, "limit": str(limit)})
56
+ return self._request("GET", f"/v1/inbox?{query}")
57
+
58
+ def reply_text(self, event_id: str, text: str, *, title: str = "") -> dict[str, Any]:
59
+ return self._request("POST", "/v1/reply", {"event_id": event_id, "kind": "text", "title": title, "text": text})
60
+
61
+ def reply_file(self, event_id: str, file_path: str | Path, *, caption: str = "", photo: bool = False) -> dict[str, Any]:
62
+ path = Path(file_path)
63
+ content = path.read_bytes()
64
+ return self._request(
65
+ "POST",
66
+ "/v1/reply",
67
+ {
68
+ "event_id": event_id,
69
+ "kind": "photo" if photo else "document",
70
+ "filename": path.name,
71
+ "content_type": mimetypes.guess_type(path.name)[0] or "application/octet-stream",
72
+ "content_base64": base64.b64encode(content).decode("ascii"),
73
+ "text": caption,
74
+ },
75
+ )
76
+
77
+ def download(self, event_id: str) -> tuple[str, str, bytes]:
78
+ query = urllib.parse.urlencode({"event_id": event_id})
79
+ result = self._request("GET", f"/v1/file?{query}")
80
+ return str(result["filename"]), str(result["content_type"]), base64.b64decode(result["content_base64"], validate=True)
81
+
82
+ def notify(self, text: str, *, title: str = "") -> dict[str, Any]:
83
+ return self._request("POST", "/v1/notify", {"kind": "text", "title": title, "text": text})
84
+
85
+
86
+ def main() -> None:
87
+ parser = argparse.ArgumentParser(description="Talk to the local Telegram agent gateway")
88
+ parser.add_argument("--source", required=True)
89
+ parser.add_argument("--key-file", required=True)
90
+ parser.add_argument("--base-url", default="http://127.0.0.1:8818")
91
+ sub = parser.add_subparsers(required=True)
92
+
93
+ inbox = sub.add_parser("inbox")
94
+ inbox.add_argument("--after", default="")
95
+ inbox.add_argument("--limit", type=int, default=50)
96
+
97
+ reply = sub.add_parser("reply")
98
+ reply.add_argument("event_id")
99
+ reply.add_argument("--text", required=True)
100
+
101
+ send_file = sub.add_parser("reply-file")
102
+ send_file.add_argument("event_id")
103
+ send_file.add_argument("file")
104
+ send_file.add_argument("--caption", default="")
105
+ send_file.add_argument("--photo", action="store_true")
106
+
107
+ download = sub.add_parser("download")
108
+ download.add_argument("event_id")
109
+ download.add_argument("--output", required=True)
110
+
111
+ notify = sub.add_parser("notify")
112
+ notify.add_argument("--text", required=True)
113
+ notify.add_argument("--title", default="")
114
+
115
+ args = parser.parse_args()
116
+ client = TelegramAgentClient.from_key_file(args.source, args.key_file, base_url=args.base_url)
117
+ if args.__dict__.get("after") is not None:
118
+ print(json.dumps(client.inbox(after=args.after, limit=args.limit), indent=2))
119
+ elif hasattr(args, "event_id") and hasattr(args, "text"):
120
+ print(json.dumps(client.reply_text(args.event_id, args.text), indent=2))
121
+ elif hasattr(args, "file"):
122
+ print(json.dumps(client.reply_file(args.event_id, args.file, caption=args.caption, photo=args.photo), indent=2))
123
+ elif hasattr(args, "output"):
124
+ filename, content_type, content = client.download(args.event_id)
125
+ output = Path(args.output)
126
+ output.write_bytes(content)
127
+ print(json.dumps({"ok": True, "filename": filename, "content_type": content_type, "output": str(output), "bytes": len(content)}))
128
+ else:
129
+ print(json.dumps(client.notify(args.text, title=args.title), indent=2))
130
+
131
+
132
+ if __name__ == "__main__":
133
+ main()
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentforge-telegram-gateway
3
+ Version: 3.1.0
4
+ Summary: Telegram bridge and MCP gateway for user-owned AI agents
5
+ Author: AgentForge Labs
6
+ License: AGPL-3.0-only
7
+ Project-URL: Homepage, https://github.com/AgentForge-Labs/shared-telegram-notifier
8
+ Project-URL: Repository, https://github.com/AgentForge-Labs/shared-telegram-notifier
9
+ Project-URL: Issues, https://github.com/AgentForge-Labs/shared-telegram-notifier/issues
10
+ Keywords: telegram,mcp,ai-agents,chatgpt,claude,codex,qwen
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest>=8; extra == "test"
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1; extra == "dev"
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # AgentForge Telegram Gateway
28
+
29
+ Connect user-owned AI agents to people through Telegram for notifications, conversations and file exchange.
30
+
31
+ **Community Edition · MVP/Beta · GNU AGPL v3 (`AGPL-3.0-only`)**
32
+
33
+ [Quickstart](#quickstart) · [Features](#features) · [MCP](#mcp-access) · [Security](#security) · [Architecture](docs/ARCHITECTURE.md) · [Contributing](CONTRIBUTING.md)
34
+
35
+ AgentForge Telegram Gateway is for developers and operators who want a small, self-hosted bridge between Telegram and local or remote AI-agent workflows. Pair a Telegram chat once, provision a source-scoped agent identity, then exchange text, documents and photos without putting a Telegram chat ID or bot token into agent prompts.
36
+
37
+ It works with MCP-capable clients and agent workflows such as ChatGPT, Codex, Claude and Qwen. The gateway is provider-independent: provider-account authentication remains with the provider's own tooling.
38
+
39
+ ## Quickstart
40
+
41
+ ### 1. Install
42
+
43
+ ```bash
44
+ git clone https://github.com/AgentForge-Labs/shared-telegram-notifier.git
45
+ cd shared-telegram-notifier
46
+ python3 -m venv .venv
47
+ . .venv/bin/activate
48
+ python -m pip install .
49
+ ```
50
+
51
+ This installs four commands:
52
+
53
+ ```text
54
+ agentforge-telegram inbox, reply, file and notification CLI
55
+ agentforge-telegram-mcp MCP diagnostics and tool access
56
+ agentforge-telegram-admin bot setup, source provisioning and pairing
57
+ agentforge-telegram-gateway gateway server
58
+ ```
59
+
60
+ ### 2. Create a Telegram bot
61
+
62
+ Create a bot with Telegram's `@BotFather`, then store the token through the protected setup flow:
63
+
64
+ ```bash
65
+ sudo agentforge-telegram-admin telegram-setup
66
+ sudo agentforge-telegram-admin telegram-doctor
67
+ ```
68
+
69
+ The bot token is entered through a hidden prompt and stored outside the repository. Do not commit bot tokens, chat IDs or generated source credentials.
70
+
71
+ ### 3. Provision and pair an agent source
72
+
73
+ ```bash
74
+ sudo agentforge-telegram-admin provision my-agent \
75
+ --output /var/lib/my-agent/telegram-gateway-key \
76
+ --owner my-agent:my-agent
77
+
78
+ sudo agentforge-telegram-admin pair my-agent
79
+ ```
80
+
81
+ Open the generated Telegram deep link. The pairing flow learns the authorized Telegram identity from Telegram itself; users do not need to copy a numeric chat ID.
82
+
83
+ ### 4. Run the gateway
84
+
85
+ A hardened systemd unit is included at [`deploy/telegram-notifier.service`](deploy/telegram-notifier.service). After installing the service and configuration paths for your host:
86
+
87
+ ```bash
88
+ sudo systemctl daemon-reload
89
+ sudo systemctl enable --now telegram-notifier
90
+ curl http://127.0.0.1:8818/health
91
+ ```
92
+
93
+ The gateway defaults to loopback. Put a TLS reverse proxy in front of it if an MCP client must connect remotely.
94
+
95
+ ## Features
96
+
97
+ - one-time Telegram pairing links — no manual chat-ID discovery;
98
+ - bidirectional text messages;
99
+ - inbound and outbound documents/photos;
100
+ - per-source `receive`, `send` and `files` permissions;
101
+ - multiple agent sources with isolated credentials and bindings;
102
+ - Telegram commands such as `/agents`, `/use`, `/status` and `/help`;
103
+ - REST/agent CLI for inbox, replies, files and proactive notifications;
104
+ - Streamable HTTP-style MCP endpoint;
105
+ - Bearer authentication for header-capable MCP clients;
106
+ - URL-secret mode for clients that cannot send custom authorization headers;
107
+ - OAuth 2.1 Authorization Code + PKCE for OAuth-capable MCP clients;
108
+ - source-scoped authorization so one agent credential cannot impersonate another source;
109
+ - loopback-first deployment and protected file-based secrets.
110
+
111
+ ## MCP access
112
+
113
+ The MCP gateway exposes the Telegram bridge to MCP-capable clients while preserving source isolation.
114
+
115
+ ### Bearer
116
+
117
+ Use the normal MCP endpoint with the source credential:
118
+
119
+ ```text
120
+ POST https://telegram.example.com/mcp
121
+ Authorization: Bearer <source-secret>
122
+ ```
123
+
124
+ ### URL secret
125
+
126
+ For clients that cannot attach an authorization header:
127
+
128
+ ```text
129
+ https://telegram.example.com/client/<source-secret>/mcp
130
+ ```
131
+
132
+ Treat that URL as a credential. Configure reverse-proxy access logs so the secret-bearing path is never recorded verbatim.
133
+
134
+ ### OAuth + PKCE
135
+
136
+ OAuth-capable MCP clients can use the gateway's protected-resource and authorization-server discovery endpoints with Authorization Code + PKCE S256. Configure OAuth with protected secret files; do not place client secrets directly in committed service files.
137
+
138
+ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the authentication and trust boundaries.
139
+
140
+ ## Generate an MCP connection file
141
+
142
+ The admin CLI can write a protected client configuration without printing the source secret to standard output:
143
+
144
+ ```bash
145
+ sudo agentforge-telegram-admin mcp-config my-agent \
146
+ --provider qwen \
147
+ --base-url https://telegram.example.com \
148
+ --key-file /var/lib/my-agent/telegram-gateway-key \
149
+ --output /var/lib/my-agent/telegram-mcp.json \
150
+ --owner my-agent:my-agent
151
+ ```
152
+
153
+ Use the provider value that matches the client you are configuring. Generated connection files are credentials and should remain outside the repository.
154
+
155
+ ## Agent CLI examples
156
+
157
+ Check the authenticated inbox:
158
+
159
+ ```bash
160
+ agentforge-telegram --help
161
+ ```
162
+
163
+ The CLI supports receiving events, replying, retrieving inbound files and sending notifications. Run `--help` for the current command surface rather than copying credentials into shell history.
164
+
165
+ ### Python SDKs
166
+
167
+ The REST client is importable directly after installing the PyPI package:
168
+
169
+ ```python
170
+ from agent_client import TelegramAgentClient
171
+
172
+ client = TelegramAgentClient.from_key_file(
173
+ "my-agent", "/var/lib/my-agent/telegram-gateway-key"
174
+ )
175
+ events = client.inbox(limit=10)
176
+ ```
177
+
178
+ For MCP transport diagnostics and arbitrary tool calls, use
179
+ `mcp_client.TelegramMcpClient` or the matching CLI command:
180
+
181
+ ```bash
182
+ agentforge-telegram-mcp --base-url https://telegram.example.com \
183
+ --auth bearer --key-file /var/lib/my-agent/telegram-gateway-key call \
184
+ telegram_inbox --arguments '{"limit":10}'
185
+ ```
186
+
187
+ ## Security
188
+
189
+ The project is designed around a simple rule: **Telegram identity, gateway identity and model-provider identity are separate trust boundaries.**
190
+
191
+ Key defaults and protections include:
192
+
193
+ - gateway TCP bind defaults to `127.0.0.1:8818`;
194
+ - the bot token lives in a protected file outside the repository;
195
+ - source credentials are generated per agent/source;
196
+ - persistent registries store credential digests rather than plaintext source keys;
197
+ - pairing codes are short-lived and one-use;
198
+ - OAuth uses exact redirect-URI matching and PKCE S256;
199
+ - OAuth authorization/access/refresh state is stored as digests where applicable;
200
+ - URL-secret application logs are redacted;
201
+ - audit metadata excludes message text, file contents and raw credentials;
202
+ - group chats are disabled by default.
203
+
204
+ Example configuration lives in [`.env.example`](.env.example) and contains placeholders/file paths only. Never commit a real bot token, numeric personal chat/user identifier, real conversation fixture, private webhook URL or generated agent credential.
205
+
206
+ For trust boundaries and protocol flow, see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). For vulnerability reporting, see [`SECURITY.md`](SECURITY.md).
207
+
208
+ ## Deployment notes
209
+
210
+ The included systemd unit is a reference for Linux hosts. Review service user/group ownership and paths before enabling it on your system. Remote MCP access should use HTTPS at the reverse proxy; do not expose the loopback HTTP listener directly to the public internet.
211
+
212
+ Useful diagnostics:
213
+
214
+ ```bash
215
+ agentforge-telegram-admin telegram-doctor
216
+ agentforge-telegram-mcp --help
217
+ agentforge-telegram-gateway --check
218
+ ```
219
+
220
+ ## What this project is — and is not
221
+
222
+ This repository is the focused Telegram communication bridge. It does not contain the AgentForge cloud control plane, private AgentScope backend, enterprise infrastructure orchestration or production credentials.
223
+
224
+ For broader AgentForge projects and public releases, visit [AgentForge Labs on GitHub](https://github.com/AgentForge-Labs).
225
+
226
+ Within the AgentForge product family:
227
+
228
+ - **AgentScope** is the natural next step when you need observability and operations around agent runs;
229
+ - **AgentForge Runtime** is the commercial expansion when you need controlled infrastructure access and enterprise runtime capabilities.
230
+
231
+ No private repository is required to use this Community Edition.
232
+
233
+ ## Contributing
234
+
235
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md). Please keep examples synthetic and never include real Telegram credentials, personal chat/user IDs or customer messages in issues, fixtures or pull requests.
236
+
237
+ ## License
238
+
239
+ AgentForge Telegram Gateway Community Edition is licensed under the **GNU Affero General Public License v3.0 only** (`AGPL-3.0-only`). See [`LICENSE`](LICENSE).
240
+
241
+ ## Maintainers
242
+
243
+ Maintained by [AgentForge Labs](https://github.com/AgentForge-Labs).
244
+
245
+ ## Commercial web application
246
+
247
+ The private commercial repository includes a separate Next.js marketing and checkout app in `web/`.
248
+
249
+ ```bash
250
+ cd web
251
+ npm ci
252
+ npm run validate
253
+ npm run dev
254
+ ```
255
+
256
+ Payment credentials and provider product/variant IDs are documented in `web/.env.example`. They are server-side only.
@@ -0,0 +1,24 @@
1
+ agent_client.py,sha256=_1Z7PBksQBcDr2tDNpWPQsFGuOEy9EsNzCSnV4AgZUE,6048
2
+ bridge_state.py,sha256=Bb9GxNPclr-EtajM_X2LoURygZcdZNyfhVJZYf0lJpk,15071
3
+ file_lock.py,sha256=I2zSZ5f8qHLaEpOYvPBze3CzAUTDt3GScBgt573zIyg,2302
4
+ mcp_auth.py,sha256=CLZLPjjRVdPJrw-ta83OWM7gy4U3XUv1CuHYWHD5ZOw,23567
5
+ mcp_client.py,sha256=4ue9bfClyj0xtTtvHprwx0sf-m_mrwd7KlgQKcT03HU,9295
6
+ mcp_protocol.py,sha256=LgDld2__HwmifqjYhhCVXyIb9AsMr5MJvuUWLec2IDY,13906
7
+ notifier_admin.py,sha256=zsCS58VobVuj-0hKp1vG-fZfNn9FUj1zwbiqvSwLAj4,21433
8
+ notifier_gateway.py,sha256=addKjP9u8jLtPgDki724wIj4J4Yo39gdqbdwW2Dvmr4,51046
9
+ provider_auth.py,sha256=kGVv7eDud3PnUWkbZ6yiQ0mV6P8oIMrKqyWMhOTho0Q,7869
10
+ __pycache__/agent_client.cpython-313.pyc,sha256=PM7FxXGodXomi8LJiAOplS81gBaS9nQF9bb3ARpZddI,10246
11
+ __pycache__/bridge_state.cpython-313.pyc,sha256=NGtcF5_yCWXDofKioENEch9frjs22alZvGK7TkXL5ME,27387
12
+ __pycache__/file_lock.cpython-313.pyc,sha256=POpx99YR-OPnfSHNnU_f1x8Yx3uAA1Cqs1vNVUs3fpU,4110
13
+ __pycache__/mcp_auth.cpython-313.pyc,sha256=yN0Va8O0W-Tj8ecsGZWwHP1FdtM5KCDSoQZKFi-cBc0,34969
14
+ __pycache__/mcp_client.cpython-313.pyc,sha256=WQ7_v0Gwac4MMHTZotyOH0UK7hugmIdb7pF9rIX09UY,13720
15
+ __pycache__/mcp_protocol.cpython-313.pyc,sha256=F6lc2_Y-vx69qbRgWrVBG1ZwVcZ1K-Vx6VxXt6zNaWI,13772
16
+ __pycache__/notifier_admin.cpython-313.pyc,sha256=aHFi1zkzR4pZc5hxhr3qdqUnHGW7gCYPUPu8Wu6O4sI,32848
17
+ __pycache__/notifier_gateway.cpython-313.pyc,sha256=DdO-J-qGF3xxZQ7IDyY54kTRCmyPv_5VagDN8q8ha6Y,76699
18
+ __pycache__/provider_auth.cpython-313.pyc,sha256=29leuaeuvUi0YzUZP1m1-bst9wnn0VoqiXfI986meog,11647
19
+ agentforge_telegram_gateway-3.1.0.dist-info/licenses/LICENSE,sha256=4O7bphXVzRuYavtsWzpLGuM3E-fp3HTRna7F4yIfnS4,35184
20
+ agentforge_telegram_gateway-3.1.0.dist-info/METADATA,sha256=XFj3DnsUl3yZKLuwaclaQ76aqUE_VVK3AA4-UskOM9A,10327
21
+ agentforge_telegram_gateway-3.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
22
+ agentforge_telegram_gateway-3.1.0.dist-info/entry_points.txt,sha256=cyZmZxsnRxJWBLwbY1Ml_QdecGmwQ3fyjaaVc-iT308,200
23
+ agentforge_telegram_gateway-3.1.0.dist-info/top_level.txt,sha256=a35DKJGeuAtoD0x3ld2wouwpbGw5zKJ4gA2ajG_8pJE,115
24
+ agentforge_telegram_gateway-3.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ agentforge-telegram = agent_client:main
3
+ agentforge-telegram-admin = notifier_admin:main
4
+ agentforge-telegram-gateway = notifier_gateway:main
5
+ agentforge-telegram-mcp = mcp_client:main