youam 0.1.0__tar.gz

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.
Files changed (71) hide show
  1. youam-0.1.0/PKG-INFO +47 -0
  2. youam-0.1.0/pyproject.toml +72 -0
  3. youam-0.1.0/setup.cfg +4 -0
  4. youam-0.1.0/src/uam/__init__.py +18 -0
  5. youam-0.1.0/src/uam/cli/__init__.py +0 -0
  6. youam-0.1.0/src/uam/cli/main.py +482 -0
  7. youam-0.1.0/src/uam/demo/__init__.py +0 -0
  8. youam-0.1.0/src/uam/demo/hello_agent.py +205 -0
  9. youam-0.1.0/src/uam/mcp/__init__.py +1 -0
  10. youam-0.1.0/src/uam/mcp/server.py +193 -0
  11. youam-0.1.0/src/uam/protocol/__init__.py +110 -0
  12. youam-0.1.0/src/uam/protocol/address.py +64 -0
  13. youam-0.1.0/src/uam/protocol/contact.py +185 -0
  14. youam-0.1.0/src/uam/protocol/crypto.py +219 -0
  15. youam-0.1.0/src/uam/protocol/envelope.py +252 -0
  16. youam-0.1.0/src/uam/protocol/errors.py +38 -0
  17. youam-0.1.0/src/uam/protocol/types.py +53 -0
  18. youam-0.1.0/src/uam/relay/__init__.py +1 -0
  19. youam-0.1.0/src/uam/relay/app.py +226 -0
  20. youam-0.1.0/src/uam/relay/auth.py +44 -0
  21. youam-0.1.0/src/uam/relay/config.py +44 -0
  22. youam-0.1.0/src/uam/relay/connections.py +74 -0
  23. youam-0.1.0/src/uam/relay/database.py +381 -0
  24. youam-0.1.0/src/uam/relay/demo_sessions.py +109 -0
  25. youam-0.1.0/src/uam/relay/heartbeat.py +99 -0
  26. youam-0.1.0/src/uam/relay/models.py +178 -0
  27. youam-0.1.0/src/uam/relay/rate_limit.py +78 -0
  28. youam-0.1.0/src/uam/relay/reputation.py +215 -0
  29. youam-0.1.0/src/uam/relay/routes/__init__.py +1 -0
  30. youam-0.1.0/src/uam/relay/routes/admin.py +213 -0
  31. youam-0.1.0/src/uam/relay/routes/agents.py +46 -0
  32. youam-0.1.0/src/uam/relay/routes/demo.py +190 -0
  33. youam-0.1.0/src/uam/relay/routes/federation.py +21 -0
  34. youam-0.1.0/src/uam/relay/routes/health.py +21 -0
  35. youam-0.1.0/src/uam/relay/routes/inbox.py +51 -0
  36. youam-0.1.0/src/uam/relay/routes/register.py +91 -0
  37. youam-0.1.0/src/uam/relay/routes/send.py +128 -0
  38. youam-0.1.0/src/uam/relay/routes/verify_domain.py +87 -0
  39. youam-0.1.0/src/uam/relay/routes/webhook_admin.py +102 -0
  40. youam-0.1.0/src/uam/relay/spam_filter.py +204 -0
  41. youam-0.1.0/src/uam/relay/verification.py +244 -0
  42. youam-0.1.0/src/uam/relay/webhook.py +344 -0
  43. youam-0.1.0/src/uam/relay/webhook_validator.py +76 -0
  44. youam-0.1.0/src/uam/relay/ws.py +248 -0
  45. youam-0.1.0/src/uam/sdk/__init__.py +6 -0
  46. youam-0.1.0/src/uam/sdk/_sync.py +55 -0
  47. youam-0.1.0/src/uam/sdk/agent.py +651 -0
  48. youam-0.1.0/src/uam/sdk/config.py +113 -0
  49. youam-0.1.0/src/uam/sdk/contact_book.py +288 -0
  50. youam-0.1.0/src/uam/sdk/dns_verifier.py +215 -0
  51. youam-0.1.0/src/uam/sdk/handshake.py +233 -0
  52. youam-0.1.0/src/uam/sdk/key_manager.py +102 -0
  53. youam-0.1.0/src/uam/sdk/message.py +50 -0
  54. youam-0.1.0/src/uam/sdk/resolver.py +111 -0
  55. youam-0.1.0/src/uam/sdk/transport/__init__.py +34 -0
  56. youam-0.1.0/src/uam/sdk/transport/base.py +39 -0
  57. youam-0.1.0/src/uam/sdk/transport/http.py +76 -0
  58. youam-0.1.0/src/uam/sdk/transport/websocket.py +148 -0
  59. youam-0.1.0/src/uam/sdk/webhook_verify.py +53 -0
  60. youam-0.1.0/src/youam.egg-info/PKG-INFO +47 -0
  61. youam-0.1.0/src/youam.egg-info/SOURCES.txt +69 -0
  62. youam-0.1.0/src/youam.egg-info/dependency_links.txt +1 -0
  63. youam-0.1.0/src/youam.egg-info/entry_points.txt +3 -0
  64. youam-0.1.0/src/youam.egg-info/requires.txt +50 -0
  65. youam-0.1.0/src/youam.egg-info/top_level.txt +1 -0
  66. youam-0.1.0/tests/test_address.py +125 -0
  67. youam-0.1.0/tests/test_contact.py +305 -0
  68. youam-0.1.0/tests/test_crypto.py +238 -0
  69. youam-0.1.0/tests/test_envelope.py +433 -0
  70. youam-0.1.0/tests/test_errors.py +75 -0
  71. youam-0.1.0/tests/test_types.py +100 -0
youam-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: youam
3
+ Version: 0.1.0
4
+ Summary: Universal Agent Messaging protocol library
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: pynacl>=1.6.2
7
+ Requires-Dist: uuid6>=2025.0.1
8
+ Requires-Dist: httpx>=0.28
9
+ Requires-Dist: websockets>=14
10
+ Requires-Dist: pydantic>=2.0
11
+ Requires-Dist: aiosqlite>=0.21
12
+ Requires-Dist: click>=8.1
13
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
14
+ Requires-Dist: dnspython>=2.8
15
+ Provides-Extra: relay
16
+ Requires-Dist: fastapi>=0.115; extra == "relay"
17
+ Requires-Dist: uvicorn[standard]>=0.34; extra == "relay"
18
+ Provides-Extra: mcp
19
+ Requires-Dist: mcp>=1.0; extra == "mcp"
20
+ Provides-Extra: demo
21
+ Requires-Dist: litellm>=1.30; extra == "demo"
22
+ Provides-Extra: docs
23
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
24
+ Requires-Dist: mkdocstrings[python]>=0.27; extra == "docs"
25
+ Requires-Dist: mkdocs-click>=0.8; extra == "docs"
26
+ Requires-Dist: mkdocs-gen-files>=0.5; extra == "docs"
27
+ Requires-Dist: mkdocs-literate-nav>=0.6; extra == "docs"
28
+ Requires-Dist: mkdocs-section-index>=0.3; extra == "docs"
29
+ Requires-Dist: mkdocs-render-swagger-plugin>=0.1; extra == "docs"
30
+ Provides-Extra: all
31
+ Requires-Dist: fastapi>=0.115; extra == "all"
32
+ Requires-Dist: uvicorn[standard]>=0.34; extra == "all"
33
+ Requires-Dist: mcp>=1.0; extra == "all"
34
+ Requires-Dist: litellm>=1.30; extra == "all"
35
+ Requires-Dist: mkdocs-material>=9.5; extra == "all"
36
+ Requires-Dist: mkdocstrings[python]>=0.27; extra == "all"
37
+ Requires-Dist: mkdocs-click>=0.8; extra == "all"
38
+ Requires-Dist: mkdocs-gen-files>=0.5; extra == "all"
39
+ Requires-Dist: mkdocs-literate-nav>=0.6; extra == "all"
40
+ Requires-Dist: mkdocs-section-index>=0.3; extra == "all"
41
+ Requires-Dist: mkdocs-render-swagger-plugin>=0.1; extra == "all"
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=8.0; extra == "dev"
44
+ Requires-Dist: pytest-cov; extra == "dev"
45
+ Requires-Dist: pytest-asyncio>=0.25; extra == "dev"
46
+ Requires-Dist: pytest-httpx>=0.35; extra == "dev"
47
+ Requires-Dist: httpx>=0.28; extra == "dev"
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "youam"
7
+ version = "0.1.0"
8
+ description = "Universal Agent Messaging protocol library"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "pynacl>=1.6.2",
12
+ "uuid6>=2025.0.1",
13
+ "httpx>=0.28",
14
+ "websockets>=14",
15
+ "pydantic>=2.0",
16
+ "aiosqlite>=0.21",
17
+ "click>=8.1",
18
+ "tomli>=2.0; python_version < '3.11'",
19
+ "dnspython>=2.8",
20
+ ]
21
+
22
+ [project.scripts]
23
+ uam = "uam.cli.main:cli"
24
+ uam-mcp = "uam.mcp.server:main"
25
+
26
+ [project.optional-dependencies]
27
+ relay = [
28
+ "fastapi>=0.115",
29
+ "uvicorn[standard]>=0.34",
30
+ ]
31
+ mcp = [
32
+ "mcp>=1.0",
33
+ ]
34
+ demo = [
35
+ "litellm>=1.30",
36
+ ]
37
+ docs = [
38
+ "mkdocs-material>=9.5",
39
+ "mkdocstrings[python]>=0.27",
40
+ "mkdocs-click>=0.8",
41
+ "mkdocs-gen-files>=0.5",
42
+ "mkdocs-literate-nav>=0.6",
43
+ "mkdocs-section-index>=0.3",
44
+ "mkdocs-render-swagger-plugin>=0.1",
45
+ ]
46
+ all = [
47
+ "fastapi>=0.115",
48
+ "uvicorn[standard]>=0.34",
49
+ "mcp>=1.0",
50
+ "litellm>=1.30",
51
+ "mkdocs-material>=9.5",
52
+ "mkdocstrings[python]>=0.27",
53
+ "mkdocs-click>=0.8",
54
+ "mkdocs-gen-files>=0.5",
55
+ "mkdocs-literate-nav>=0.6",
56
+ "mkdocs-section-index>=0.3",
57
+ "mkdocs-render-swagger-plugin>=0.1",
58
+ ]
59
+ dev = [
60
+ "pytest>=8.0",
61
+ "pytest-cov",
62
+ "pytest-asyncio>=0.25",
63
+ "pytest-httpx>=0.35",
64
+ "httpx>=0.28",
65
+ ]
66
+
67
+ [tool.setuptools.packages.find]
68
+ where = ["src"]
69
+
70
+ [tool.pytest.ini_options]
71
+ testpaths = ["tests"]
72
+ asyncio_mode = "auto"
youam-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ """UAM -- Universal Agent Messaging.
2
+
3
+ Top-level convenience re-exports::
4
+
5
+ from uam import Agent, ReceivedMessage
6
+ from uam.protocol import MessageType, create_envelope # protocol functions
7
+ """
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ try:
12
+ from uam.sdk.agent import Agent
13
+ from uam.sdk.message import ReceivedMessage
14
+
15
+ __all__ = ["__version__", "Agent", "ReceivedMessage"]
16
+ except ImportError:
17
+ # SDK dependencies (httpx, websockets) not installed -- protocol-only usage
18
+ __all__ = ["__version__"]
File without changes
@@ -0,0 +1,482 @@
1
+ """UAM CLI -- Universal Agent Messaging command-line interface.
2
+
3
+ Thin wrapper around the Python SDK using click.
4
+ All commands use sync wrappers (send_sync, inbox_sync, connect_sync, close_sync).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import click
15
+
16
+ from uam.protocol import UAMError
17
+ from uam.protocol.crypto import public_key_fingerprint
18
+ from uam.sdk.agent import Agent
19
+ from uam.sdk.config import SDKConfig
20
+ from uam.sdk.contact_book import ContactBook
21
+ from uam.sdk.key_manager import KeyManager
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Helpers
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ def _find_agent_name(key_dir: Path | None = None) -> str | None:
30
+ """Scan key directory for a .key file and return the agent name.
31
+
32
+ If exactly one .key file exists, return the name (filename without .key).
33
+ If multiple exist, return the first alphabetically.
34
+ If none exist, return None.
35
+ """
36
+ if key_dir is None:
37
+ cfg = SDKConfig(name="_probe")
38
+ key_dir = cfg.key_dir
39
+ key_dir = Path(key_dir)
40
+ if not key_dir.exists():
41
+ return None
42
+ key_files = sorted(key_dir.glob("*.key"))
43
+ if not key_files:
44
+ return None
45
+ return key_files[0].stem
46
+
47
+
48
+ def _error(msg: str) -> None:
49
+ """Print an error message to stderr and exit 1."""
50
+ click.echo(msg, err=True)
51
+ raise SystemExit(1)
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # CLI group
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ @click.group()
60
+ @click.version_option(package_name="uam")
61
+ @click.option(
62
+ "--name",
63
+ "-n",
64
+ default=None,
65
+ help="Agent name (auto-detected from ~/.uam/keys/).",
66
+ )
67
+ @click.pass_context
68
+ def cli(ctx: click.Context, name: str | None) -> None:
69
+ """UAM -- Universal Agent Messaging CLI."""
70
+ ctx.ensure_object(dict)
71
+ ctx.obj["name"] = name
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # uam init (CLI-01)
76
+ # ---------------------------------------------------------------------------
77
+
78
+
79
+ @cli.command()
80
+ @click.option("--name", "-n", default=None, help="Agent name.")
81
+ @click.option(
82
+ "--relay", "-r", default=None, help="Relay URL (default: relay.youam.network)."
83
+ )
84
+ @click.pass_context
85
+ def init(ctx: click.Context, name: str | None, relay: str | None) -> None:
86
+ """Initialize a new agent: generate keys and register with relay."""
87
+ agent_name = name or ctx.obj.get("name")
88
+ if not agent_name:
89
+ import socket
90
+
91
+ agent_name = socket.gethostname().split(".")[0].lower()
92
+
93
+ try:
94
+ # Check if already initialized
95
+ cfg = SDKConfig(name=agent_name, relay_url=relay)
96
+ km = KeyManager(cfg.key_dir)
97
+ key_path = Path(cfg.key_dir) / f"{agent_name}.key"
98
+
99
+ if key_path.exists():
100
+ km.load_or_generate(agent_name)
101
+ address = f"{agent_name}::{cfg.relay_domain}"
102
+ fp = public_key_fingerprint(km.verify_key)
103
+ click.echo(f"Agent already initialized: {address}")
104
+ click.echo(f"Fingerprint: {fp}")
105
+ return
106
+
107
+ # New agent -- connect to register
108
+ agent = Agent(agent_name, relay=relay)
109
+ agent.connect_sync()
110
+ address = agent.address
111
+ fp = public_key_fingerprint(agent._key_manager.verify_key)
112
+ agent.close_sync()
113
+ click.echo(f"Initialized agent: {address}")
114
+ click.echo(f"Fingerprint: {fp}")
115
+ except UAMError as exc:
116
+ _error(f"Error: {exc}")
117
+ except Exception as exc:
118
+ _error(f"Error: {exc}")
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # uam send (CLI-02)
123
+ # ---------------------------------------------------------------------------
124
+
125
+
126
+ @cli.command()
127
+ @click.argument("address")
128
+ @click.argument("message")
129
+ @click.pass_context
130
+ def send(ctx: click.Context, address: str, message: str) -> None:
131
+ """Send a message to another agent."""
132
+ agent_name = ctx.obj.get("name") or _find_agent_name()
133
+ if not agent_name:
134
+ _error("No agent initialized. Run `uam init` first.")
135
+
136
+ try:
137
+ agent = Agent(agent_name)
138
+ agent.connect_sync()
139
+ msg_id = agent.send_sync(address, message)
140
+ agent.close_sync()
141
+ click.echo(f"Message sent to {address} (id: {msg_id})")
142
+ except UAMError as exc:
143
+ _error(f"Error: {exc}")
144
+ except RuntimeError as exc:
145
+ _error(f"Error: {exc}")
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # uam inbox (CLI-03)
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ @cli.command()
154
+ @click.option("--limit", "-l", default=20, help="Max messages to retrieve.")
155
+ @click.pass_context
156
+ def inbox(ctx: click.Context, limit: int) -> None:
157
+ """Check your inbox for pending messages."""
158
+ agent_name = ctx.obj.get("name") or _find_agent_name()
159
+ if not agent_name:
160
+ _error("No agent initialized. Run `uam init` first.")
161
+
162
+ try:
163
+ agent = Agent(agent_name)
164
+ agent.connect_sync()
165
+ messages = agent.inbox_sync(limit=limit)
166
+ agent.close_sync()
167
+
168
+ if not messages:
169
+ click.echo("No pending messages.")
170
+ return
171
+
172
+ for msg in messages:
173
+ click.echo(f"From: {msg.from_address}")
174
+ click.echo(f"Time: {msg.timestamp}")
175
+ click.echo("---")
176
+ click.echo(msg.content)
177
+ click.echo()
178
+ except UAMError as exc:
179
+ _error(f"Error: {exc}")
180
+ except RuntimeError as exc:
181
+ _error(f"Error: {exc}")
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # uam whoami (CLI-04)
186
+ # ---------------------------------------------------------------------------
187
+
188
+
189
+ @cli.command()
190
+ @click.pass_context
191
+ def whoami(ctx: click.Context) -> None:
192
+ """Display your agent address and public key fingerprint (offline)."""
193
+ agent_name = ctx.obj.get("name") or _find_agent_name()
194
+ if not agent_name:
195
+ _error("No agent initialized. Run `uam init` first.")
196
+
197
+ cfg = SDKConfig(name=agent_name)
198
+ key_path = Path(cfg.key_dir) / f"{agent_name}.key"
199
+ if not key_path.exists():
200
+ _error("No agent initialized. Run `uam init` first.")
201
+
202
+ km = KeyManager(cfg.key_dir)
203
+ km.load_or_generate(agent_name)
204
+ address = f"{agent_name}::{cfg.relay_domain}"
205
+ fp = public_key_fingerprint(km.verify_key)
206
+
207
+ click.echo(f"Address: {address}")
208
+ click.echo(f"Fingerprint: {fp}")
209
+ click.echo(f"Key file: {key_path}")
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # uam contacts (CLI-05)
214
+ # ---------------------------------------------------------------------------
215
+
216
+
217
+ @cli.command()
218
+ @click.pass_context
219
+ def contacts(ctx: click.Context) -> None:
220
+ """List known contacts from the local contact book."""
221
+ agent_name = ctx.obj.get("name") or _find_agent_name()
222
+
223
+ # Determine data_dir
224
+ cfg = SDKConfig(name=agent_name or "_probe")
225
+ book = ContactBook(cfg.data_dir)
226
+
227
+ try:
228
+ rows = asyncio.run(_list_contacts(book))
229
+ except Exception:
230
+ rows = []
231
+
232
+ if not rows:
233
+ click.echo("No contacts yet.")
234
+ return
235
+
236
+ # Print table header
237
+ click.echo(f"{'ADDRESS':<30} {'TRUST':<18} {'LAST SEEN'}")
238
+ for row in rows:
239
+ addr = row["address"]
240
+ trust = row["trust_state"]
241
+ last = row["last_seen"] or ""
242
+ click.echo(f"{addr:<30} {trust:<18} {last}")
243
+
244
+
245
+ async def _list_contacts(book: ContactBook) -> list[dict]:
246
+ """Open contact book, list contacts, close."""
247
+ await book.open()
248
+ try:
249
+ return await book.list_contacts()
250
+ finally:
251
+ await book.close()
252
+
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # uam card (CLAW-02)
256
+ # ---------------------------------------------------------------------------
257
+
258
+
259
+ @cli.command()
260
+ @click.pass_context
261
+ def card(ctx: click.Context) -> None:
262
+ """Display your signed contact card as JSON."""
263
+ agent_name = ctx.obj.get("name") or _find_agent_name()
264
+ if not agent_name:
265
+ _error("No agent initialized. Run 'uam init' first.")
266
+
267
+ try:
268
+ agent = Agent(agent_name)
269
+ agent.connect_sync()
270
+ card_dict = agent.contact_card()
271
+ agent.close_sync()
272
+ click.echo(json.dumps(card_dict, indent=2))
273
+ except UAMError as exc:
274
+ _error(f"Error: {exc}")
275
+ except RuntimeError as exc:
276
+ _error(f"Error: {exc}")
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # uam pending (HAND-06)
281
+ # ---------------------------------------------------------------------------
282
+
283
+
284
+ @cli.command()
285
+ @click.pass_context
286
+ def pending(ctx: click.Context) -> None:
287
+ """List pending handshake requests awaiting approval."""
288
+ agent_name = ctx.obj.get("name") or _find_agent_name()
289
+ if not agent_name:
290
+ _error("No agent initialized. Run `uam init` first.")
291
+
292
+ try:
293
+ agent = Agent(agent_name)
294
+ agent.connect_sync()
295
+ items = agent.pending_sync()
296
+ agent.close_sync()
297
+
298
+ if not items:
299
+ click.echo("No pending handshake requests.")
300
+ return
301
+
302
+ click.echo(f"{'ADDRESS':<35} {'RECEIVED'}")
303
+ for item in items:
304
+ addr = item["address"]
305
+ received = item.get("received_at", "")
306
+ click.echo(f"{addr:<35} {received}")
307
+ except UAMError as exc:
308
+ _error(f"Error: {exc}")
309
+ except RuntimeError as exc:
310
+ _error(f"Error: {exc}")
311
+
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # uam approve (HAND-06)
315
+ # ---------------------------------------------------------------------------
316
+
317
+
318
+ @cli.command()
319
+ @click.argument("address")
320
+ @click.pass_context
321
+ def approve(ctx: click.Context, address: str) -> None:
322
+ """Approve a pending handshake request."""
323
+ agent_name = ctx.obj.get("name") or _find_agent_name()
324
+ if not agent_name:
325
+ _error("No agent initialized. Run `uam init` first.")
326
+
327
+ try:
328
+ agent = Agent(agent_name)
329
+ agent.connect_sync()
330
+ agent.approve_sync(address)
331
+ agent.close_sync()
332
+ click.echo(f"Approved: {address}")
333
+ except UAMError as exc:
334
+ _error(f"Error: {exc}")
335
+ except RuntimeError as exc:
336
+ _error(f"Error: {exc}")
337
+
338
+
339
+ # ---------------------------------------------------------------------------
340
+ # uam deny (HAND-06)
341
+ # ---------------------------------------------------------------------------
342
+
343
+
344
+ @cli.command()
345
+ @click.argument("address")
346
+ @click.pass_context
347
+ def deny(ctx: click.Context, address: str) -> None:
348
+ """Deny a pending handshake request."""
349
+ agent_name = ctx.obj.get("name") or _find_agent_name()
350
+ if not agent_name:
351
+ _error("No agent initialized. Run `uam init` first.")
352
+
353
+ try:
354
+ agent = Agent(agent_name)
355
+ agent.connect_sync()
356
+ agent.deny_sync(address)
357
+ agent.close_sync()
358
+ click.echo(f"Denied: {address}")
359
+ except UAMError as exc:
360
+ _error(f"Error: {exc}")
361
+ except RuntimeError as exc:
362
+ _error(f"Error: {exc}")
363
+
364
+
365
+ # ---------------------------------------------------------------------------
366
+ # uam block (HAND-06)
367
+ # ---------------------------------------------------------------------------
368
+
369
+
370
+ @cli.command()
371
+ @click.argument("pattern")
372
+ @click.pass_context
373
+ def block(ctx: click.Context, pattern: str) -> None:
374
+ """Block an address or domain pattern (e.g., spammer::evil.com or *::evil.com)."""
375
+ agent_name = ctx.obj.get("name") or _find_agent_name()
376
+ cfg = SDKConfig(name=agent_name or "_probe")
377
+ book = ContactBook(cfg.data_dir)
378
+
379
+ try:
380
+ asyncio.run(_do_block(book, pattern))
381
+ click.echo(f"Blocked: {pattern}")
382
+ except Exception as exc:
383
+ _error(f"Error: {exc}")
384
+
385
+
386
+ async def _do_block(book: ContactBook, pattern: str) -> None:
387
+ """Open contact book, add block, close."""
388
+ await book.open()
389
+ try:
390
+ await book.add_block(pattern)
391
+ finally:
392
+ await book.close()
393
+
394
+
395
+ # ---------------------------------------------------------------------------
396
+ # uam unblock (HAND-06)
397
+ # ---------------------------------------------------------------------------
398
+
399
+
400
+ @cli.command()
401
+ @click.argument("pattern")
402
+ @click.pass_context
403
+ def unblock(ctx: click.Context, pattern: str) -> None:
404
+ """Remove a block on an address or domain pattern."""
405
+ agent_name = ctx.obj.get("name") or _find_agent_name()
406
+ cfg = SDKConfig(name=agent_name or "_probe")
407
+ book = ContactBook(cfg.data_dir)
408
+
409
+ try:
410
+ asyncio.run(_do_unblock(book, pattern))
411
+ click.echo(f"Unblocked: {pattern}")
412
+ except Exception as exc:
413
+ _error(f"Error: {exc}")
414
+
415
+
416
+ async def _do_unblock(book: ContactBook, pattern: str) -> None:
417
+ """Open contact book, remove block, close."""
418
+ await book.open()
419
+ try:
420
+ await book.remove_block(pattern)
421
+ finally:
422
+ await book.close()
423
+
424
+
425
+ # ---------------------------------------------------------------------------
426
+ # uam verify-domain (DNS-05)
427
+ # ---------------------------------------------------------------------------
428
+
429
+
430
+ @cli.command("verify-domain")
431
+ @click.argument("domain")
432
+ @click.option("--timeout", "-t", default=300, help="Polling timeout in seconds.")
433
+ @click.option("--poll-interval", default=10, help="Polling interval in seconds.")
434
+ @click.pass_context
435
+ def verify_domain(ctx: click.Context, domain: str, timeout: int, poll_interval: int) -> None:
436
+ """Verify domain ownership for Tier 2 DNS-verified status."""
437
+ from uam.sdk.dns_verifier import generate_txt_record
438
+
439
+ agent_name = ctx.obj.get("name") or _find_agent_name()
440
+ if not agent_name:
441
+ _error("No agent initialized. Run `uam init` first.")
442
+
443
+ try:
444
+ agent = Agent(agent_name)
445
+ agent.connect_sync()
446
+
447
+ pubkey = agent.public_key
448
+ relay_url = agent._config.relay_url
449
+ txt_value = generate_txt_record(pubkey, relay_url)
450
+
451
+ click.echo(f"Add this DNS TXT record to verify {domain}:")
452
+ click.echo()
453
+ click.echo(f" Host: _uam.{domain}")
454
+ click.echo(f" Type: TXT")
455
+ click.echo(f" Value: {txt_value}")
456
+ click.echo()
457
+ click.echo(f"Or serve this HTTPS fallback:")
458
+ click.echo()
459
+ click.echo(f" URL: https://{domain}/.well-known/uam.json")
460
+ click.echo()
461
+ click.echo(f"See documentation for .well-known/uam.json format.")
462
+ click.echo()
463
+ click.echo("Polling for verification...")
464
+
465
+ verified = agent.verify_domain_sync(
466
+ domain, timeout=timeout, poll_interval=poll_interval
467
+ )
468
+ agent.close_sync()
469
+
470
+ if verified:
471
+ click.echo(
472
+ f"Verified! {agent.address} is now Tier 2 via {domain}."
473
+ )
474
+ else:
475
+ click.echo(
476
+ f"Verification timed out after {timeout}s. "
477
+ f"Check your DNS records and try again."
478
+ )
479
+ except UAMError as exc:
480
+ _error(f"Error: {exc}")
481
+ except RuntimeError as exc:
482
+ _error(f"Error: {exc}")
File without changes