the-ment 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.
the_ment-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Environment Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: the-ment
3
+ Version: 0.1.0
4
+ Summary: A multi-agent environment with identities and simple usage
5
+ Author: The Environment Contributors
6
+ License: MIT
7
+ Keywords: agent,identity,cryptography,ed25519,authentication,multi-agent
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: cryptography>=46.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # The Environment
28
+
29
+ Portable, pseudonymous agent identities with Ed25519 cryptographic signatures.
30
+
31
+ A minimal protocol for secure agent discovery, authentication, and communication. Perfect for multi-agent systems, AI agents, and decentralized applications.
32
+
33
+ ## Why The Environment?
34
+
35
+ - **Secure** - Ed25519 signatures, AES-GCM encryption, Scrypt KDF
36
+ - **Simple** - Small core with one cryptography dependency
37
+ - **Portable** - Export/import identities as bundles
38
+ - **Flexible** - Support any serializable Python data
39
+ - **Fast** - In-memory environment for real-time discovery
40
+
41
+ ## Quick Start
42
+
43
+ ```bash
44
+ pip install the-ment
45
+ ```
46
+
47
+ ### Create an Identity
48
+
49
+ ```python
50
+ from ment import create_identity
51
+
52
+ identity = create_identity("my-password")
53
+ print(f"Agent ID: {identity.id}")
54
+ unlocked = identity.unlock("my-password")
55
+ ```
56
+
57
+ ### Sign and Verify
58
+
59
+ ```python
60
+ from ment import create_identity, verify_signature
61
+
62
+ identity = create_identity("my-password")
63
+ unlocked = identity.unlock("my-password")
64
+ signature = unlocked.sign({"action": "transfer"})
65
+
66
+ is_valid = verify_signature(identity.public_key, {"action": "transfer"}, signature)
67
+ print(f"Valid: {is_valid}") # True
68
+ ```
69
+
70
+ ### Multi-Agent Communication
71
+
72
+ ```python
73
+ from ment import create_identity, Environment
74
+
75
+ agent_a = create_identity("password-a")
76
+ agent_b = create_identity("password-b")
77
+
78
+ env = Environment()
79
+ env.join(agent_a.id, capabilities=["code"])
80
+ env.join(agent_b.id, capabilities=["search"])
81
+
82
+ env.send(agent_a.id, {"text": "Hello!"}, target_id=agent_b.id)
83
+ messages = env.receive(agent_b.id)
84
+ print(messages[0].content) # {"text": "Hello!"}
85
+ ```
86
+
87
+ For authenticated delivery, register the sender's public key and send a signed
88
+ `Message` through `env.send_message(...)`. The Environment verifies the
89
+ identity binding and signature before delivery. Unsigned messages remain
90
+ available for open discovery and communication.
91
+
92
+ ## Examples
93
+
94
+ ```bash
95
+ python examples/basic_usage.py
96
+ python examples/multi_agent_chat.py
97
+ python examples/signed_messages.py
98
+ ```
99
+
100
+ ## Minimal API
101
+
102
+ The Environment can also run as a small in-memory HTTP service:
103
+
104
+ ```python
105
+ from ment import serve
106
+
107
+ serve(host="127.0.0.1", port=8765)
108
+ ```
109
+
110
+ Available endpoints:
111
+
112
+ ```text
113
+ POST /join
114
+ GET /discover
115
+ POST /send
116
+ GET /receive
117
+ POST /heartbeat
118
+ POST /leave
119
+ ```
120
+
121
+ The API has no database or graphical interface. It exposes the same temporary
122
+ Environment through JSON so agents in different processes can communicate.
123
+
124
+ ## Architecture
125
+
126
+ - **Identity**: Random Ed25519 key pair, password-protected via AES-GCM
127
+ - **Signatures**: Ed25519 for message authentication
128
+ - **Environment**: In-memory agent discovery and messaging with automatic verification of supplied signatures
129
+ - **Bundles**: Portable export/import of full identity
130
+
131
+ ## Security
132
+
133
+ - Ed25519 signatures (fast, secure, small)
134
+ - AES-GCM encryption for private keys
135
+ - Scrypt KDF for password protection
136
+ - Constant-time comparison for ID verification
137
+ - Input length limits on identity and message source fields
138
+
139
+ ## Testing
140
+
141
+ ```bash
142
+ python -m pytest tests/ -v
143
+ ```
144
+
145
+ 20/20 tests passing
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,123 @@
1
+ # The Environment
2
+
3
+ Portable, pseudonymous agent identities with Ed25519 cryptographic signatures.
4
+
5
+ A minimal protocol for secure agent discovery, authentication, and communication. Perfect for multi-agent systems, AI agents, and decentralized applications.
6
+
7
+ ## Why The Environment?
8
+
9
+ - **Secure** - Ed25519 signatures, AES-GCM encryption, Scrypt KDF
10
+ - **Simple** - Small core with one cryptography dependency
11
+ - **Portable** - Export/import identities as bundles
12
+ - **Flexible** - Support any serializable Python data
13
+ - **Fast** - In-memory environment for real-time discovery
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ pip install the-ment
19
+ ```
20
+
21
+ ### Create an Identity
22
+
23
+ ```python
24
+ from ment import create_identity
25
+
26
+ identity = create_identity("my-password")
27
+ print(f"Agent ID: {identity.id}")
28
+ unlocked = identity.unlock("my-password")
29
+ ```
30
+
31
+ ### Sign and Verify
32
+
33
+ ```python
34
+ from ment import create_identity, verify_signature
35
+
36
+ identity = create_identity("my-password")
37
+ unlocked = identity.unlock("my-password")
38
+ signature = unlocked.sign({"action": "transfer"})
39
+
40
+ is_valid = verify_signature(identity.public_key, {"action": "transfer"}, signature)
41
+ print(f"Valid: {is_valid}") # True
42
+ ```
43
+
44
+ ### Multi-Agent Communication
45
+
46
+ ```python
47
+ from ment import create_identity, Environment
48
+
49
+ agent_a = create_identity("password-a")
50
+ agent_b = create_identity("password-b")
51
+
52
+ env = Environment()
53
+ env.join(agent_a.id, capabilities=["code"])
54
+ env.join(agent_b.id, capabilities=["search"])
55
+
56
+ env.send(agent_a.id, {"text": "Hello!"}, target_id=agent_b.id)
57
+ messages = env.receive(agent_b.id)
58
+ print(messages[0].content) # {"text": "Hello!"}
59
+ ```
60
+
61
+ For authenticated delivery, register the sender's public key and send a signed
62
+ `Message` through `env.send_message(...)`. The Environment verifies the
63
+ identity binding and signature before delivery. Unsigned messages remain
64
+ available for open discovery and communication.
65
+
66
+ ## Examples
67
+
68
+ ```bash
69
+ python examples/basic_usage.py
70
+ python examples/multi_agent_chat.py
71
+ python examples/signed_messages.py
72
+ ```
73
+
74
+ ## Minimal API
75
+
76
+ The Environment can also run as a small in-memory HTTP service:
77
+
78
+ ```python
79
+ from ment import serve
80
+
81
+ serve(host="127.0.0.1", port=8765)
82
+ ```
83
+
84
+ Available endpoints:
85
+
86
+ ```text
87
+ POST /join
88
+ GET /discover
89
+ POST /send
90
+ GET /receive
91
+ POST /heartbeat
92
+ POST /leave
93
+ ```
94
+
95
+ The API has no database or graphical interface. It exposes the same temporary
96
+ Environment through JSON so agents in different processes can communicate.
97
+
98
+ ## Architecture
99
+
100
+ - **Identity**: Random Ed25519 key pair, password-protected via AES-GCM
101
+ - **Signatures**: Ed25519 for message authentication
102
+ - **Environment**: In-memory agent discovery and messaging with automatic verification of supplied signatures
103
+ - **Bundles**: Portable export/import of full identity
104
+
105
+ ## Security
106
+
107
+ - Ed25519 signatures (fast, secure, small)
108
+ - AES-GCM encryption for private keys
109
+ - Scrypt KDF for password protection
110
+ - Constant-time comparison for ID verification
111
+ - Input length limits on identity and message source fields
112
+
113
+ ## Testing
114
+
115
+ ```bash
116
+ python -m pytest tests/ -v
117
+ ```
118
+
119
+ 20/20 tests passing
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,32 @@
1
+ """Agent ID - Portable, pseudonymous agent identities with Ed25519 signatures."""
2
+
3
+ from core.agent_core import (
4
+ DEFAULT_H,
5
+ AgentIdentity,
6
+ Message,
7
+ UnlockedIdentity,
8
+ create_identity,
9
+ derive_id,
10
+ verify_identity,
11
+ verify_signature,
12
+ )
13
+
14
+ from core.environment import Environment, Presence
15
+ from core.api import create_server, serve
16
+
17
+ __all__ = [
18
+ "DEFAULT_H",
19
+ "AgentIdentity",
20
+ "Message",
21
+ "UnlockedIdentity",
22
+ "create_identity",
23
+ "derive_id",
24
+ "verify_identity",
25
+ "verify_signature",
26
+ "Environment",
27
+ "Presence",
28
+ "create_server",
29
+ "serve",
30
+ ]
31
+
32
+ __version__ = "0.1.0"
@@ -0,0 +1,263 @@
1
+ """Small, password-protected identities and signed agent messages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hmac
6
+ from dataclasses import dataclass, field, replace
7
+ from hashlib import sha256
8
+ import json
9
+ import secrets
10
+ import time
11
+ from typing import Any
12
+
13
+ from cryptography.exceptions import InvalidSignature, InvalidTag
14
+ from cryptography.hazmat.primitives import serialization
15
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
16
+ Ed25519PrivateKey,
17
+ Ed25519PublicKey,
18
+ )
19
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
20
+ from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
21
+
22
+
23
+ SALT_SIZE = 16
24
+ NONCE_SIZE = 12
25
+ SCRYPT_N = 2**14
26
+ SCRYPT_R = 8
27
+ SCRYPT_P = 1
28
+ MAX_PASSWORD_LENGTH = 1024
29
+ MAX_STRING_LENGTH = 4096
30
+
31
+
32
+ def _require_text(value: str, name: str, max_length: int = MAX_STRING_LENGTH) -> str:
33
+ if not isinstance(value, str) or not value:
34
+ raise ValueError(f"{name} must be a non-empty string")
35
+ if len(value) > max_length:
36
+ raise ValueError(f"{name} exceeds maximum length of {max_length}")
37
+ return value
38
+
39
+
40
+ def _hash_parts(*parts: str) -> str:
41
+ encoded = bytearray()
42
+ for part in parts:
43
+ value = _require_text(part, "hash part").encode("utf-8")
44
+ encoded.extend(len(value).to_bytes(8, "big"))
45
+ encoded.extend(value)
46
+ return sha256(encoded).hexdigest()
47
+
48
+
49
+ def _canonical_bytes(value: Any) -> bytes:
50
+ return json.dumps(
51
+ value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
52
+ ).encode("utf-8")
53
+
54
+
55
+ def _public_key_bytes(public_key: str) -> bytes:
56
+ try:
57
+ raw = bytes.fromhex(_require_text(public_key, "public_key"))
58
+ Ed25519PublicKey.from_public_bytes(raw)
59
+ return raw
60
+ except (ValueError, TypeError):
61
+ raise ValueError("public_key must be a valid Ed25519 public key") from None
62
+
63
+
64
+ def _derive_unlock_key(password: str, salt: bytes) -> bytes:
65
+ _require_text(password, "password", MAX_PASSWORD_LENGTH)
66
+ return Scrypt(
67
+ salt=salt,
68
+ length=32,
69
+ n=SCRYPT_N,
70
+ r=SCRYPT_R,
71
+ p=SCRYPT_P,
72
+ ).derive(password.encode("utf-8"))
73
+
74
+
75
+ DEFAULT_H = sha256(b"agent-id-protocol-v1").hexdigest()
76
+
77
+
78
+ def derive_id(public_key: str, H: str = DEFAULT_H) -> str:
79
+ """Derive the stable public ID from the protocol base and public key."""
80
+ _public_key_bytes(public_key)
81
+ return _hash_parts(H, public_key)
82
+
83
+
84
+ def verify_identity(identity_id: str, public_key: str, H: str = DEFAULT_H) -> bool:
85
+ """Verify that an ID belongs to a specific public key."""
86
+ _require_text(identity_id, "identity_id")
87
+ derived = derive_id(public_key, H)
88
+ return hmac.compare_digest(derived, identity_id)
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class AgentIdentity:
93
+ """Public identity plus an encrypted private signing key."""
94
+
95
+ id: str
96
+ public_key: str
97
+ encrypted_private_key: str = field(repr=False)
98
+ salt: str = field(repr=False)
99
+ nonce: str = field(repr=False)
100
+ H: str = DEFAULT_H
101
+
102
+ @property
103
+ def public(self) -> dict[str, str]:
104
+ return {"H": self.H, "id": self.id, "public_key": self.public_key}
105
+
106
+ def bundle(self) -> dict[str, str]:
107
+ """Return the data needed to store and restore this identity."""
108
+ return {
109
+ **self.public,
110
+ "encrypted_private_key": self.encrypted_private_key,
111
+ "salt": self.salt,
112
+ "nonce": self.nonce,
113
+ }
114
+
115
+ @classmethod
116
+ def from_bundle(cls, bundle: dict[str, str]) -> "AgentIdentity":
117
+ identity = cls(
118
+ id=bundle["id"],
119
+ public_key=bundle["public_key"],
120
+ encrypted_private_key=bundle["encrypted_private_key"],
121
+ salt=bundle["salt"],
122
+ nonce=bundle["nonce"],
123
+ H=bundle["H"],
124
+ )
125
+ if not verify_identity(identity.id, identity.public_key, identity.H):
126
+ raise ValueError("identity does not match its public key")
127
+ return identity
128
+
129
+ def _aad(self) -> bytes:
130
+ return _canonical_bytes(self.public)
131
+
132
+ def _private_key(self, password: str) -> Ed25519PrivateKey:
133
+ try:
134
+ key = _derive_unlock_key(password, bytes.fromhex(self.salt))
135
+ raw = AESGCM(key).decrypt(
136
+ bytes.fromhex(self.nonce),
137
+ bytes.fromhex(self.encrypted_private_key),
138
+ self._aad(),
139
+ )
140
+ private_key = Ed25519PrivateKey.from_private_bytes(raw)
141
+ if private_key.public_key().public_bytes(
142
+ serialization.Encoding.Raw,
143
+ serialization.PublicFormat.Raw,
144
+ ).hex() != self.public_key:
145
+ raise ValueError("private key does not match identity")
146
+ return private_key
147
+ except (InvalidTag, ValueError, TypeError):
148
+ raise ValueError("invalid password or identity bundle") from None
149
+
150
+ def unlock(self, password: str) -> "UnlockedIdentity":
151
+ """Unlock the private key once for repeated agent operations."""
152
+ return UnlockedIdentity(self.id, self.public_key, self._private_key(password))
153
+
154
+ def sign(self, value: Any, password: str) -> str:
155
+ """Sign data with one-off password unlocking (compatibility API)."""
156
+ return self.unlock(password).sign(value)
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class UnlockedIdentity:
161
+ """In-memory signing session created by AgentIdentity.unlock."""
162
+
163
+ id: str
164
+ public_key: str
165
+ _private_key: Ed25519PrivateKey = field(repr=False)
166
+
167
+ @property
168
+ def public(self) -> dict[str, str]:
169
+ return {"id": self.id, "public_key": self.public_key}
170
+
171
+ def sign(self, value: Any) -> str:
172
+ """Sign data without asking for the password again."""
173
+ return self._private_key.sign(_canonical_bytes(value)).hex()
174
+
175
+
176
+ def create_identity(password: str, H: str = DEFAULT_H) -> AgentIdentity:
177
+ """Create a random signing key protected by a password."""
178
+ _require_text(password, "password")
179
+ private_key = Ed25519PrivateKey.generate()
180
+ public_key = private_key.public_key().public_bytes(
181
+ serialization.Encoding.Raw,
182
+ serialization.PublicFormat.Raw,
183
+ ).hex()
184
+ private_raw = private_key.private_bytes(
185
+ serialization.Encoding.Raw,
186
+ serialization.PrivateFormat.Raw,
187
+ serialization.NoEncryption(),
188
+ )
189
+ salt = secrets.token_bytes(SALT_SIZE)
190
+ nonce = secrets.token_bytes(NONCE_SIZE)
191
+ identity_id = derive_id(public_key, H)
192
+ temporary = AgentIdentity(
193
+ id=identity_id,
194
+ public_key=public_key,
195
+ encrypted_private_key="",
196
+ salt=salt.hex(),
197
+ nonce=nonce.hex(),
198
+ H=H,
199
+ )
200
+ unlock_key = _derive_unlock_key(password, salt)
201
+ encrypted = AESGCM(unlock_key).encrypt(nonce, private_raw, temporary._aad())
202
+ return replace(temporary, encrypted_private_key=encrypted.hex())
203
+
204
+
205
+ def verify_signature(public_key: str, value: Any, signature: str) -> bool:
206
+ """Verify an Ed25519 signature over canonical data."""
207
+ try:
208
+ _public_key_bytes(public_key)
209
+ Ed25519PublicKey.from_public_bytes(bytes.fromhex(public_key)).verify(
210
+ bytes.fromhex(signature), _canonical_bytes(value)
211
+ )
212
+ return True
213
+ except (InvalidSignature, ValueError, TypeError):
214
+ return False
215
+
216
+
217
+ @dataclass(frozen=True)
218
+ class Message:
219
+ """A generic signed or unsigned message; target_id is optional."""
220
+
221
+ source_id: str
222
+ content: Any
223
+ target_id: str | None = None
224
+ t: int = field(default_factory=lambda: time.time_ns() // 1_000_000)
225
+ signature: str | None = None
226
+
227
+ def __post_init__(self) -> None:
228
+ _require_text(self.source_id, "source_id")
229
+ if self.target_id is not None:
230
+ _require_text(self.target_id, "target_id")
231
+
232
+ def _unsigned(self) -> dict[str, Any]:
233
+ return {
234
+ "source_id": self.source_id,
235
+ "target_id": self.target_id,
236
+ "content": self.content,
237
+ "t": self.t,
238
+ }
239
+
240
+ def sign(
241
+ self,
242
+ identity: AgentIdentity | UnlockedIdentity,
243
+ password: str | None = None,
244
+ ) -> "Message":
245
+ if self.source_id != identity.id:
246
+ raise ValueError("message source does not match identity")
247
+ if isinstance(identity, UnlockedIdentity):
248
+ if password is not None:
249
+ raise ValueError("password is not used by an unlocked identity")
250
+ signer = identity
251
+ else:
252
+ if password is None:
253
+ raise ValueError("password is required to unlock the identity")
254
+ signer = identity.unlock(password)
255
+ return replace(self, signature=signer.sign(self._unsigned()))
256
+
257
+ def verify(self, public_key: str) -> bool:
258
+ return self.signature is not None and verify_signature(
259
+ public_key, self._unsigned(), self.signature
260
+ )
261
+
262
+ def to_dict(self) -> dict[str, Any]:
263
+ return {**self._unsigned(), "signature": self.signature}