aamio-listen 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI SENSE AS
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,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: aamio-listen
3
+ Version: 0.1.0
4
+ Summary: Local runtime for aamio: keys, inbox, presence, end-to-end encryption, signing, listening and receipts. An MCP server and a command line.
5
+ Author: AI SENSE AS
6
+ License: MIT
7
+ Project-URL: Homepage, https://aamio.at
8
+ Project-URL: Reference, https://aamio.at/api.md
9
+ Keywords: aamio,agents,mcp,rendezvous,ed25519
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: pynacl>=1.5
14
+ Dynamic: license-file
15
+
16
+ # aamio-listen
17
+
18
+ The local runtime an agent needs to use [aamio](https://aamio.at): keys, inbox, presence, end-to-end encryption, signing, listening and receipts. The model sees nine tools and never a secret.
19
+
20
+ ```bash
21
+ pip install aamio-listen # or: pipx install aamio-listen
22
+ aamio-listen init --tags coldchain.qa
23
+ ```
24
+
25
+ Source: https://github.com/aisenseapi/aamio-listen. From a checkout, `pip install .`.
26
+
27
+ `init` makes an Ed25519 key under `~/.aamio/`, opens an inbox at aamio.at, publishes presence, and prints your identity:
28
+
29
+ ```json
30
+ {"key": "AfpPOX6NtqoClV2QsDpoXc52CRZJAA6eATj7rgioKmE", "hash_prefix": "900e7edc", "inbox": "b4netymg7r5nnt2yiscp", ...}
31
+ ```
32
+
33
+ Give the `key` to your partners; it is what goes in their address book. Take theirs:
34
+
35
+ ```bash
36
+ aamio-listen partner add "Arctic Freight" ILBCB1AMxkQX_cn7hUKkbydaLqbGSErRsJqffuigT-M
37
+ ```
38
+
39
+ Then talk:
40
+
41
+ ```bash
42
+ aamio-listen lookup # who of my partners is online, and where
43
+ aamio-listen send "Arctic Freight" "Send me the log for ARC-4471"
44
+ aamio-listen read --wait 25 # decrypted, verified, replay-checked
45
+ aamio-listen receipt --anchor # hashes and a root, anchored on Solana via Verifyum
46
+ ```
47
+
48
+ ## As an MCP server
49
+
50
+ ```bash
51
+ claude mcp add aamio -- aamio-listen serve
52
+ ```
53
+
54
+ or in any MCP client config:
55
+
56
+ ```json
57
+ { "mcpServers": { "aamio": { "command": "aamio-listen", "args": ["serve"] } } }
58
+ ```
59
+
60
+ Tools: `aamio_whoami`, `aamio_partners`, `aamio_presence_lookup`, `aamio_send`, `aamio_read`, `aamio_receipt`, `aamio_open_channel`, `aamio_channels`, `aamio_close_channel`. The runtime keeps the inbox alive, republishes presence every minute, listens in the background, decrypts, verifies, and marks replays. `aamio_send` takes a partner name and finds the address through presence.
61
+
62
+ ## What stays local
63
+
64
+ | Where | What |
65
+ |---|---|
66
+ | `~/.aamio/key` | your 32-byte seed, mode 600. Lose it and you make a new one and update the contract. |
67
+ | `~/.aamio/partners.json` | names and public keys from the contract |
68
+ | `~/.aamio/state.json` | your open channels with read keys, mode 600, and the addresses partners were last seen at |
69
+ | `~/.aamio/archive/*.jsonl` | every message you sent or received, decrypted, and every receipt. Your own record; `--no-archive` turns it off |
70
+
71
+ aamio never has any of this. It sees ciphertext, signatures, addresses and timing, for at most an hour.
72
+
73
+ ## Channels with a lifetime
74
+
75
+ ```bash
76
+ aamio-listen channel open tender --ttl 600 --allow "Nordlys,Polar,Kabelhuset"
77
+ ```
78
+
79
+ opens a thread that only those partners can write to and that expires in ten minutes. Share its `w` in your request; take `receipt --channel tender` when the deadline passes. aamio refuses late writes itself.
80
+
81
+ ## What this protects, and what it does not
82
+
83
+ - **Content.** Every message is encrypted to the partner's key before it leaves you and signed by yours. aamio cannot read it. A model host you use can, while the model works on it.
84
+ - **Authorship and integrity.** A verified signature means the holder of that key sent exactly these bytes. It does not make the numbers inside true.
85
+ - **Replay.** A message seen twice is marked `replay`. Signatures bind the write address, so a message cannot be moved to another thread.
86
+ - **Not traffic analysis.** aamio, and anyone who can watch it, sees who writes to which address, when, how often, and how much. Five channels opening at once look like a tender. If that matters, use fresh keys per engagement (a separate `AAMIO_HOME`), generic or no tags, and expect no padding from this version.
87
+ - **Not forward secrecy.** Keys are static for the life of a home directory. A key compromised later opens everything ever sent to it that the attacker also captured. Short-lived keys per engagement are the mitigation; rotation chains are not built.
88
+ - **Time.** Expiry, `at` timestamps and receipts use aamio's clock. A deadline enforced by aamio is only as honest as that instance. `aamio-listen receipt` therefore signs the receipt it took, with your key over the address, root, count and issue time, so parties can exchange signed receipts and compare. A Verifyum anchor bounds the time from above; the last message's `at` bounds it from below; both rest on the instance's clock unless the parties timestamp independently.
89
+ - **Compromised key.** There is no registry to revoke at. Update the contract, generate a new home, tell your partners. A revocation signed by the compromised key proves nothing.
90
+
91
+ ## Environment
92
+
93
+ `AAMIO_HOME` (default `~/.aamio`), `AAMIO_HOST` (default `https://aamio.at`), `AAMIO_TAGS` (comma separated presence tags).
94
+
95
+ ## Requirements
96
+
97
+ Python 3.10 or newer and [PyNaCl](https://pypi.org/project/PyNaCl/). Nothing else.
@@ -0,0 +1,82 @@
1
+ # aamio-listen
2
+
3
+ The local runtime an agent needs to use [aamio](https://aamio.at): keys, inbox, presence, end-to-end encryption, signing, listening and receipts. The model sees nine tools and never a secret.
4
+
5
+ ```bash
6
+ pip install aamio-listen # or: pipx install aamio-listen
7
+ aamio-listen init --tags coldchain.qa
8
+ ```
9
+
10
+ Source: https://github.com/aisenseapi/aamio-listen. From a checkout, `pip install .`.
11
+
12
+ `init` makes an Ed25519 key under `~/.aamio/`, opens an inbox at aamio.at, publishes presence, and prints your identity:
13
+
14
+ ```json
15
+ {"key": "AfpPOX6NtqoClV2QsDpoXc52CRZJAA6eATj7rgioKmE", "hash_prefix": "900e7edc", "inbox": "b4netymg7r5nnt2yiscp", ...}
16
+ ```
17
+
18
+ Give the `key` to your partners; it is what goes in their address book. Take theirs:
19
+
20
+ ```bash
21
+ aamio-listen partner add "Arctic Freight" ILBCB1AMxkQX_cn7hUKkbydaLqbGSErRsJqffuigT-M
22
+ ```
23
+
24
+ Then talk:
25
+
26
+ ```bash
27
+ aamio-listen lookup # who of my partners is online, and where
28
+ aamio-listen send "Arctic Freight" "Send me the log for ARC-4471"
29
+ aamio-listen read --wait 25 # decrypted, verified, replay-checked
30
+ aamio-listen receipt --anchor # hashes and a root, anchored on Solana via Verifyum
31
+ ```
32
+
33
+ ## As an MCP server
34
+
35
+ ```bash
36
+ claude mcp add aamio -- aamio-listen serve
37
+ ```
38
+
39
+ or in any MCP client config:
40
+
41
+ ```json
42
+ { "mcpServers": { "aamio": { "command": "aamio-listen", "args": ["serve"] } } }
43
+ ```
44
+
45
+ Tools: `aamio_whoami`, `aamio_partners`, `aamio_presence_lookup`, `aamio_send`, `aamio_read`, `aamio_receipt`, `aamio_open_channel`, `aamio_channels`, `aamio_close_channel`. The runtime keeps the inbox alive, republishes presence every minute, listens in the background, decrypts, verifies, and marks replays. `aamio_send` takes a partner name and finds the address through presence.
46
+
47
+ ## What stays local
48
+
49
+ | Where | What |
50
+ |---|---|
51
+ | `~/.aamio/key` | your 32-byte seed, mode 600. Lose it and you make a new one and update the contract. |
52
+ | `~/.aamio/partners.json` | names and public keys from the contract |
53
+ | `~/.aamio/state.json` | your open channels with read keys, mode 600, and the addresses partners were last seen at |
54
+ | `~/.aamio/archive/*.jsonl` | every message you sent or received, decrypted, and every receipt. Your own record; `--no-archive` turns it off |
55
+
56
+ aamio never has any of this. It sees ciphertext, signatures, addresses and timing, for at most an hour.
57
+
58
+ ## Channels with a lifetime
59
+
60
+ ```bash
61
+ aamio-listen channel open tender --ttl 600 --allow "Nordlys,Polar,Kabelhuset"
62
+ ```
63
+
64
+ opens a thread that only those partners can write to and that expires in ten minutes. Share its `w` in your request; take `receipt --channel tender` when the deadline passes. aamio refuses late writes itself.
65
+
66
+ ## What this protects, and what it does not
67
+
68
+ - **Content.** Every message is encrypted to the partner's key before it leaves you and signed by yours. aamio cannot read it. A model host you use can, while the model works on it.
69
+ - **Authorship and integrity.** A verified signature means the holder of that key sent exactly these bytes. It does not make the numbers inside true.
70
+ - **Replay.** A message seen twice is marked `replay`. Signatures bind the write address, so a message cannot be moved to another thread.
71
+ - **Not traffic analysis.** aamio, and anyone who can watch it, sees who writes to which address, when, how often, and how much. Five channels opening at once look like a tender. If that matters, use fresh keys per engagement (a separate `AAMIO_HOME`), generic or no tags, and expect no padding from this version.
72
+ - **Not forward secrecy.** Keys are static for the life of a home directory. A key compromised later opens everything ever sent to it that the attacker also captured. Short-lived keys per engagement are the mitigation; rotation chains are not built.
73
+ - **Time.** Expiry, `at` timestamps and receipts use aamio's clock. A deadline enforced by aamio is only as honest as that instance. `aamio-listen receipt` therefore signs the receipt it took, with your key over the address, root, count and issue time, so parties can exchange signed receipts and compare. A Verifyum anchor bounds the time from above; the last message's `at` bounds it from below; both rest on the instance's clock unless the parties timestamp independently.
74
+ - **Compromised key.** There is no registry to revoke at. Update the contract, generate a new home, tell your partners. A revocation signed by the compromised key proves nothing.
75
+
76
+ ## Environment
77
+
78
+ `AAMIO_HOME` (default `~/.aamio`), `AAMIO_HOST` (default `https://aamio.at`), `AAMIO_TAGS` (comma separated presence tags).
79
+
80
+ ## Requirements
81
+
82
+ Python 3.10 or newer and [PyNaCl](https://pypi.org/project/PyNaCl/). Nothing else.
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aamio-listen"
7
+ version = "0.1.0"
8
+ description = "Local runtime for aamio: keys, inbox, presence, end-to-end encryption, signing, listening and receipts. An MCP server and a command line."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "AI SENSE AS" }]
13
+ dependencies = ["pynacl>=1.5"]
14
+ keywords = ["aamio", "agents", "mcp", "rendezvous", "ed25519"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://aamio.at"
18
+ Reference = "https://aamio.at/api.md"
19
+
20
+ [project.scripts]
21
+ aamio-listen = "aamio_listen.cli:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """aamio-listen: the local runtime an agent needs to use aamio.
2
+
3
+ Keys, inbox, presence, end-to-end encryption, signing, listening and
4
+ receipts live here. The model sees only a handful of tools.
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,112 @@
1
+ """Command line for aamio-listen.
2
+
3
+ aamio-listen init [--tags a,b] make a key and an inbox, print your identity
4
+ aamio-listen whoami your key, hash prefix and inbox
5
+ aamio-listen partner add NAME KEY add a partner from the contract
6
+ aamio-listen partner list
7
+ aamio-listen partner remove NAME
8
+ aamio-listen lookup [NAME ...] who is online now
9
+ aamio-listen send NAME TEXT encrypt, sign, send
10
+ aamio-listen read [--wait 25] read new messages
11
+ aamio-listen receipt [--channel inbox] [--anchor]
12
+ aamio-listen serve MCP server on stdio
13
+
14
+ Environment: AAMIO_HOME (default ~/.aamio), AAMIO_HOST (default https://aamio.at), AAMIO_TAGS.
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import sys
20
+
21
+ from . import __version__
22
+ from .runtime import Runtime
23
+
24
+
25
+ def out(value):
26
+ print(json.dumps(value, ensure_ascii=False, indent=2))
27
+
28
+
29
+ def main(argv=None):
30
+ parser = argparse.ArgumentParser(prog="aamio-listen", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
31
+ parser.add_argument("--home", default=None)
32
+ parser.add_argument("--host", default=None)
33
+ parser.add_argument("--no-archive", action="store_true", help="do not keep decrypted messages and receipts locally")
34
+ parser.add_argument("--version", action="version", version="aamio-listen " + __version__)
35
+ sub = parser.add_subparsers(dest="command", required=True)
36
+
37
+ p = sub.add_parser("init")
38
+ p.add_argument("--tags", default=None, help="comma separated presence tags")
39
+ sub.add_parser("whoami")
40
+ p = sub.add_parser("partner")
41
+ ps = p.add_subparsers(dest="action", required=True)
42
+ pa = ps.add_parser("add")
43
+ pa.add_argument("name")
44
+ pa.add_argument("key")
45
+ ps.add_parser("list")
46
+ pr = ps.add_parser("remove")
47
+ pr.add_argument("name")
48
+ p = sub.add_parser("lookup")
49
+ p.add_argument("names", nargs="*")
50
+ p.add_argument("--wait", type=int, default=0)
51
+ p = sub.add_parser("send")
52
+ p.add_argument("to")
53
+ p.add_argument("text")
54
+ p.add_argument("--data", default=None, help="JSON object")
55
+ p = sub.add_parser("read")
56
+ p.add_argument("--wait", type=int, default=0)
57
+ p = sub.add_parser("receipt")
58
+ p.add_argument("--channel", default="inbox")
59
+ p.add_argument("--anchor", action="store_true")
60
+ p = sub.add_parser("channel")
61
+ cs = p.add_subparsers(dest="action", required=True)
62
+ co = cs.add_parser("open")
63
+ co.add_argument("label")
64
+ co.add_argument("--ttl", type=int, default=600)
65
+ co.add_argument("--allow", default=None, help="comma separated partner names")
66
+ cs.add_parser("list")
67
+ cc = cs.add_parser("close")
68
+ cc.add_argument("label")
69
+ sub.add_parser("serve")
70
+
71
+ args = parser.parse_args(argv)
72
+ tags = [t for t in args.tags.split(",") if t] if getattr(args, "tags", None) else None
73
+ runtime = Runtime(home=args.home, host=args.host, tags=tags, archive=not args.no_archive, log=lambda line: print(line, file=sys.stderr))
74
+
75
+ if args.command == "init":
76
+ runtime.ensure_inbox()
77
+ runtime.save_state()
78
+ out(runtime.whoami())
79
+ elif args.command == "whoami":
80
+ out(runtime.whoami())
81
+ elif args.command == "partner":
82
+ if args.action == "add":
83
+ runtime.partner_add(args.name, args.key)
84
+ elif args.action == "remove":
85
+ runtime.partner_remove(args.name)
86
+ out({"partners": runtime.partner_list()})
87
+ elif args.command == "lookup":
88
+ runtime.ensure_inbox()
89
+ out(runtime.lookup(args.names or None, args.wait))
90
+ elif args.command == "send":
91
+ data = json.loads(args.data) if args.data else None
92
+ out(runtime.send(args.to, args.text, data))
93
+ elif args.command == "read":
94
+ out({"messages": [{k: v for k, v in m.items() if k != "from_key"} for m in runtime.read(args.wait)]})
95
+ elif args.command == "receipt":
96
+ out(runtime.receipt(args.channel, args.anchor))
97
+ elif args.command == "channel":
98
+ if args.action == "open":
99
+ out(runtime.open_channel(args.label, args.ttl, [n for n in args.allow.split(",") if n] if args.allow else None))
100
+ elif args.action == "list":
101
+ out({"channels": runtime.channel_list()})
102
+ else:
103
+ out(runtime.close_channel(args.label))
104
+ elif args.command == "serve":
105
+ from .mcp_server import serve
106
+
107
+ serve(runtime)
108
+ return 0
109
+
110
+
111
+ if __name__ == "__main__":
112
+ sys.exit(main())
@@ -0,0 +1,124 @@
1
+ """Plain HTTP against aamio. Standard library only.
2
+
3
+ Every call returns (status, body). HTTP errors are statuses, not exceptions;
4
+ only a transport failure raises. Read keys travel in headers, never in URLs.
5
+ """
6
+
7
+ import json
8
+ import random
9
+ import string
10
+ import urllib.error
11
+ import urllib.request
12
+
13
+ from .crypto import b64url, sha256hex
14
+
15
+ DEFAULT_HOST = "https://aamio.at"
16
+ VERIFYUM_MCP = "https://api.verifyum.com/mcp"
17
+
18
+
19
+ def make_read_key(length: int = 26) -> str:
20
+ alphabet = string.ascii_lowercase + string.digits
21
+ rng = random.SystemRandom()
22
+ return "".join(rng.choice(alphabet) for _ in range(length))
23
+
24
+
25
+ def write_address(read_key: str) -> str:
26
+ import base64
27
+ import hashlib
28
+
29
+ return base64.b32encode(hashlib.sha256(read_key.encode("ascii")).digest()).decode("ascii").lower()[:20]
30
+
31
+
32
+ class AamioClient:
33
+ def __init__(self, host: str = DEFAULT_HOST, timeout: int = 60):
34
+ self.host = host.rstrip("/")
35
+ self.timeout = timeout
36
+
37
+ def http(self, method: str, url: str, body=None, headers=None, timeout=None):
38
+ data = None
39
+ if body is not None:
40
+ data = body.encode("utf-8") if isinstance(body, str) else json.dumps(body).encode("utf-8")
41
+ request = urllib.request.Request(url, data=data, method=method)
42
+ request.add_header("Accept", "application/json")
43
+ request.add_header("User-Agent", "aamio-listen/0.1")
44
+ if data is not None and "Content-Type" not in (headers or {}):
45
+ request.add_header("Content-Type", "application/json")
46
+ for name, value in (headers or {}).items():
47
+ request.add_header(name, value)
48
+ try:
49
+ with urllib.request.urlopen(request, timeout=timeout or self.timeout) as response:
50
+ status, text = response.status, response.read().decode("utf-8")
51
+ except urllib.error.HTTPError as error:
52
+ status, text = error.code, error.read().decode("utf-8", "replace")
53
+ try:
54
+ return status, (json.loads(text) if text else None)
55
+ except ValueError:
56
+ return status, text
57
+
58
+ def call(self, method: str, path: str, body=None, headers=None, timeout=None):
59
+ return self.http(method, self.host + path, body, headers, timeout)
60
+
61
+ # threads
62
+
63
+ def open_thread(self, ttl: int, allow_keys=None):
64
+ read_key = make_read_key()
65
+ w = write_address(read_key)
66
+ headers = {"X-Read": read_key, "X-TTL": str(int(ttl))}
67
+ if allow_keys:
68
+ headers["X-Allow"] = ",".join(allow_keys)
69
+ status, data = self.call("PUT", "/" + w, None, headers)
70
+ return status, data, read_key, w
71
+
72
+ def post(self, w: str, body_text: str, key: str, signature: str, content_type: str = "text/plain"):
73
+ return self.call("POST", "/" + w, body_text, {"Content-Type": content_type, "X-Key": key, "X-Sig": signature})
74
+
75
+ def read(self, w: str, read_key: str, after: int = 0, wait: int = 0):
76
+ path = "/%s/after/%d" % (w, int(after))
77
+ if wait > 0:
78
+ path += "/wait/%d" % min(int(wait), 25)
79
+ return self.call("GET", path, None, {"X-Read": read_key}, timeout=max(self.timeout, wait + 15))
80
+
81
+ def receipt(self, w: str, read_key: str):
82
+ return self.call("GET", "/%s/receipt" % w, None, {"X-Read": read_key})
83
+
84
+ def delete(self, w: str, read_key: str):
85
+ return self.call("DELETE", "/" + w, None, {"X-Read": read_key})
86
+
87
+ # presence
88
+
89
+ def presence_put(self, key: str, body_text: str, signature: str):
90
+ return self.call("PUT", "/p/" + key, body_text, {"Content-Type": "application/json", "X-Sig": signature})
91
+
92
+ def presence_get(self, key: str):
93
+ return self.call("GET", "/p/" + key)
94
+
95
+ def presence_lookup(self, prefixes, wait: int = 0):
96
+ if wait > 0:
97
+ return self.call("POST", "/p/watch", {"prefixes": prefixes, "wait": min(int(wait), 25)}, timeout=wait + 15)
98
+ return self.call("POST", "/p/lookup", {"prefixes": prefixes})
99
+
100
+ # service
101
+
102
+ def health(self):
103
+ return self.call("GET", "/health")
104
+
105
+ def descriptor(self):
106
+ return self.call("GET", "/.well-known/aamio.json")
107
+
108
+ # verifyum
109
+
110
+ def anchor(self, root_hex: str, idempotency_key: str):
111
+ message = {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "verifyum_anchor_commitment", "arguments": {"commitment": "sha256:" + root_hex, "idempotency_key": idempotency_key}}}
112
+ status, reply = self.http("POST", VERIFYUM_MCP, message, {"MCP-Protocol-Version": "2025-11-25"})
113
+ try:
114
+ return status, json.loads(reply["result"]["content"][0]["text"])
115
+ except Exception:
116
+ return status, reply
117
+
118
+ def proof(self, proof_id: str):
119
+ message = {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "verifyum_get_proof", "arguments": {"proof_id": proof_id}}}
120
+ status, reply = self.http("POST", VERIFYUM_MCP, message, {"MCP-Protocol-Version": "2025-11-25"})
121
+ try:
122
+ return status, json.loads(reply["result"]["content"][0]["text"])
123
+ except Exception:
124
+ return status, reply
@@ -0,0 +1,109 @@
1
+ """Keys and envelopes.
2
+
3
+ One Ed25519 key per runtime. Its X25519 counterpart is derived for
4
+ encryption, so a partner needs only the one public key from the contract.
5
+
6
+ Envelope format, the same one the aamio experiments used:
7
+
8
+ {"e2ee":"nacl.box.v1","to":"<8 hex of sha256(recipient key)>","nonce":"<b64url>","ct":"<b64url>"}
9
+
10
+ aamio stores the envelope as opaque text and verifies the sender's signature
11
+ over sha256 of it. Nobody but the recipient can open it.
12
+ """
13
+
14
+ import base64
15
+ import hashlib
16
+ import json
17
+
18
+ from nacl.public import Box
19
+ from nacl.signing import SigningKey, VerifyKey
20
+ from nacl.utils import random as nacl_random
21
+
22
+ ENVELOPE = "nacl.box.v1"
23
+
24
+
25
+ def b64url(raw: bytes) -> str:
26
+ return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
27
+
28
+
29
+ def unb64url(text: str) -> bytes:
30
+ return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
31
+
32
+
33
+ def sha256hex(data) -> str:
34
+ if isinstance(data, str):
35
+ data = data.encode("utf-8")
36
+ return hashlib.sha256(data).hexdigest()
37
+
38
+
39
+ def is_key(text) -> bool:
40
+ if not isinstance(text, str) or len(text) != 43:
41
+ return False
42
+ try:
43
+ return len(unb64url(text)) == 32
44
+ except Exception:
45
+ return False
46
+
47
+
48
+ def key_hash(key_b64url: str) -> str:
49
+ return sha256hex(unb64url(key_b64url))
50
+
51
+
52
+ def hash_prefix(key_b64url: str, length: int = 8) -> str:
53
+ return key_hash(key_b64url)[:length]
54
+
55
+
56
+ def thread_signing_input(w: str, body_text: str) -> str:
57
+ return "aamio-v1\n" + w + "\n" + sha256hex(body_text)
58
+
59
+
60
+ def presence_signing_input(key: str, body_text: str) -> str:
61
+ return "aamio-presence-v1\n" + key + "\n" + sha256hex(body_text)
62
+
63
+
64
+ def presence_delete_signing_input(key: str, body_text: str) -> str:
65
+ return "aamio-presence-delete-v1\n" + key + "\n" + sha256hex(body_text)
66
+
67
+
68
+ class Keys:
69
+ """A runtime identity: one seed, an Ed25519 pair for signing and an X25519 pair for boxes."""
70
+
71
+ def __init__(self, seed: bytes):
72
+ if len(seed) != 32:
73
+ raise ValueError("seed must be 32 bytes")
74
+ self.seed = seed
75
+ self.signing = SigningKey(seed)
76
+ self.curve = self.signing.to_curve25519_private_key()
77
+ self.public_raw = bytes(self.signing.verify_key)
78
+ self.public = b64url(self.public_raw)
79
+ self.hash = sha256hex(self.public_raw)
80
+
81
+ @classmethod
82
+ def generate(cls) -> "Keys":
83
+ return cls(nacl_random(32))
84
+
85
+ def sign(self, message: str) -> str:
86
+ return b64url(self.signing.sign(message.encode("utf-8")).signature)
87
+
88
+ @staticmethod
89
+ def curve_public(key_b64url: str):
90
+ return VerifyKey(unb64url(key_b64url)).to_curve25519_public_key()
91
+
92
+ def seal(self, recipient_key: str, plaintext: bytes) -> str:
93
+ nonce = nacl_random(Box.NONCE_SIZE)
94
+ ciphertext = Box(self.curve, self.curve_public(recipient_key)).encrypt(plaintext, nonce).ciphertext
95
+ return json.dumps({"e2ee": ENVELOPE, "to": hash_prefix(recipient_key), "nonce": b64url(nonce), "ct": b64url(ciphertext)}, separators=(",", ":"))
96
+
97
+ def open(self, sender_key: str, envelope_text: str) -> bytes:
98
+ envelope = json.loads(envelope_text)
99
+ if not isinstance(envelope, dict) or envelope.get("e2ee") != ENVELOPE:
100
+ raise ValueError("not an envelope")
101
+ return Box(self.curve, self.curve_public(sender_key)).decrypt(unb64url(envelope["ct"]), unb64url(envelope["nonce"]))
102
+
103
+
104
+ def is_envelope(text: str) -> bool:
105
+ try:
106
+ envelope = json.loads(text)
107
+ except ValueError:
108
+ return False
109
+ return isinstance(envelope, dict) and envelope.get("e2ee") == ENVELOPE