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.
- youam-0.1.0/PKG-INFO +47 -0
- youam-0.1.0/pyproject.toml +72 -0
- youam-0.1.0/setup.cfg +4 -0
- youam-0.1.0/src/uam/__init__.py +18 -0
- youam-0.1.0/src/uam/cli/__init__.py +0 -0
- youam-0.1.0/src/uam/cli/main.py +482 -0
- youam-0.1.0/src/uam/demo/__init__.py +0 -0
- youam-0.1.0/src/uam/demo/hello_agent.py +205 -0
- youam-0.1.0/src/uam/mcp/__init__.py +1 -0
- youam-0.1.0/src/uam/mcp/server.py +193 -0
- youam-0.1.0/src/uam/protocol/__init__.py +110 -0
- youam-0.1.0/src/uam/protocol/address.py +64 -0
- youam-0.1.0/src/uam/protocol/contact.py +185 -0
- youam-0.1.0/src/uam/protocol/crypto.py +219 -0
- youam-0.1.0/src/uam/protocol/envelope.py +252 -0
- youam-0.1.0/src/uam/protocol/errors.py +38 -0
- youam-0.1.0/src/uam/protocol/types.py +53 -0
- youam-0.1.0/src/uam/relay/__init__.py +1 -0
- youam-0.1.0/src/uam/relay/app.py +226 -0
- youam-0.1.0/src/uam/relay/auth.py +44 -0
- youam-0.1.0/src/uam/relay/config.py +44 -0
- youam-0.1.0/src/uam/relay/connections.py +74 -0
- youam-0.1.0/src/uam/relay/database.py +381 -0
- youam-0.1.0/src/uam/relay/demo_sessions.py +109 -0
- youam-0.1.0/src/uam/relay/heartbeat.py +99 -0
- youam-0.1.0/src/uam/relay/models.py +178 -0
- youam-0.1.0/src/uam/relay/rate_limit.py +78 -0
- youam-0.1.0/src/uam/relay/reputation.py +215 -0
- youam-0.1.0/src/uam/relay/routes/__init__.py +1 -0
- youam-0.1.0/src/uam/relay/routes/admin.py +213 -0
- youam-0.1.0/src/uam/relay/routes/agents.py +46 -0
- youam-0.1.0/src/uam/relay/routes/demo.py +190 -0
- youam-0.1.0/src/uam/relay/routes/federation.py +21 -0
- youam-0.1.0/src/uam/relay/routes/health.py +21 -0
- youam-0.1.0/src/uam/relay/routes/inbox.py +51 -0
- youam-0.1.0/src/uam/relay/routes/register.py +91 -0
- youam-0.1.0/src/uam/relay/routes/send.py +128 -0
- youam-0.1.0/src/uam/relay/routes/verify_domain.py +87 -0
- youam-0.1.0/src/uam/relay/routes/webhook_admin.py +102 -0
- youam-0.1.0/src/uam/relay/spam_filter.py +204 -0
- youam-0.1.0/src/uam/relay/verification.py +244 -0
- youam-0.1.0/src/uam/relay/webhook.py +344 -0
- youam-0.1.0/src/uam/relay/webhook_validator.py +76 -0
- youam-0.1.0/src/uam/relay/ws.py +248 -0
- youam-0.1.0/src/uam/sdk/__init__.py +6 -0
- youam-0.1.0/src/uam/sdk/_sync.py +55 -0
- youam-0.1.0/src/uam/sdk/agent.py +651 -0
- youam-0.1.0/src/uam/sdk/config.py +113 -0
- youam-0.1.0/src/uam/sdk/contact_book.py +288 -0
- youam-0.1.0/src/uam/sdk/dns_verifier.py +215 -0
- youam-0.1.0/src/uam/sdk/handshake.py +233 -0
- youam-0.1.0/src/uam/sdk/key_manager.py +102 -0
- youam-0.1.0/src/uam/sdk/message.py +50 -0
- youam-0.1.0/src/uam/sdk/resolver.py +111 -0
- youam-0.1.0/src/uam/sdk/transport/__init__.py +34 -0
- youam-0.1.0/src/uam/sdk/transport/base.py +39 -0
- youam-0.1.0/src/uam/sdk/transport/http.py +76 -0
- youam-0.1.0/src/uam/sdk/transport/websocket.py +148 -0
- youam-0.1.0/src/uam/sdk/webhook_verify.py +53 -0
- youam-0.1.0/src/youam.egg-info/PKG-INFO +47 -0
- youam-0.1.0/src/youam.egg-info/SOURCES.txt +69 -0
- youam-0.1.0/src/youam.egg-info/dependency_links.txt +1 -0
- youam-0.1.0/src/youam.egg-info/entry_points.txt +3 -0
- youam-0.1.0/src/youam.egg-info/requires.txt +50 -0
- youam-0.1.0/src/youam.egg-info/top_level.txt +1 -0
- youam-0.1.0/tests/test_address.py +125 -0
- youam-0.1.0/tests/test_contact.py +305 -0
- youam-0.1.0/tests/test_crypto.py +238 -0
- youam-0.1.0/tests/test_envelope.py +433 -0
- youam-0.1.0/tests/test_errors.py +75 -0
- 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,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
|