ememdev 2.4.2__tar.gz → 2.4.5__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.
- {ememdev-2.4.2 → ememdev-2.4.5}/.gitignore +4 -0
- ememdev-2.4.5/PKG-INFO +231 -0
- ememdev-2.4.5/README.md +201 -0
- {ememdev-2.4.2 → ememdev-2.4.5}/pyproject.toml +1 -1
- {ememdev-2.4.2 → ememdev-2.4.5}/src/ememdev/__main__.py +11 -6
- {ememdev-2.4.2 → ememdev-2.4.5}/src/ememdev/signing.py +26 -16
- ememdev-2.4.2/PKG-INFO +0 -177
- ememdev-2.4.2/README.md +0 -147
- {ememdev-2.4.2 → ememdev-2.4.5}/LICENSE +0 -0
- {ememdev-2.4.2 → ememdev-2.4.5}/src/ememdev/__init__.py +0 -0
- {ememdev-2.4.2 → ememdev-2.4.5}/src/ememdev/client.py +0 -0
- {ememdev-2.4.2 → ememdev-2.4.5}/src/ememdev/identity.py +0 -0
- {ememdev-2.4.2 → ememdev-2.4.5}/src/ememdev/verify.py +0 -0
|
@@ -90,3 +90,7 @@ examples/3d-worlds/out/
|
|
|
90
90
|
# Agent scratch notes belong in .well-known/agent-notes/, never the repo root.
|
|
91
91
|
/AGENT_*.md
|
|
92
92
|
web/channel.json
|
|
93
|
+
|
|
94
|
+
# Baked on the node by scripts/build_channel.py every few minutes and read
|
|
95
|
+
# from disk; the binary carries web/channel-fallback.html instead.
|
|
96
|
+
web/channel.html
|
ememdev-2.4.5/PKG-INFO
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ememdev
|
|
3
|
+
Version: 2.4.5
|
|
4
|
+
Summary: Python client for emem: shared, verifiable memory for AI agents. One address per fact, one signature you can check. No key to read.
|
|
5
|
+
Project-URL: Homepage, https://emem.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/Vortx-AI/emem
|
|
7
|
+
Project-URL: Documentation, https://emem.dev/agents.md
|
|
8
|
+
Project-URL: Issues, https://github.com/Vortx-AI/emem/issues
|
|
9
|
+
Author-email: Vortx AI Private Limited <avijeet@vortx.ai>
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent-memory,ai-agents,content-addressed,earth-observation,ed25519,geospatial,mcp,model-context-protocol,provenance,satellite,shared-memory,verifiable
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
26
|
+
Provides-Extra: signing
|
|
27
|
+
Requires-Dist: blake3>=0.3; extra == 'signing'
|
|
28
|
+
Requires-Dist: cryptography>=41; extra == 'signing'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# ememdev: Python client for emem.dev
|
|
32
|
+
|
|
33
|
+
<!-- mcp-name: io.github.Vortx-AI/emem -->
|
|
34
|
+
|
|
35
|
+
`ememdev` is the Python client for the [emem.dev](https://emem.dev) Earth
|
|
36
|
+
memory protocol. Use it when a Python program or agent must read signed
|
|
37
|
+
Earth-observation facts and keep the receipts for citation. Reads need no key.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install ememdev
|
|
41
|
+
python -c 'from ememdev import Client; print(Client().locate("Mount Fuji")["cell64"])'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The second line prints the cell64 address of Mount Fuji (`defi.zb592.nUkO.zEzE`
|
|
45
|
+
on 7 November 2026). For the protocol itself, read
|
|
46
|
+
[docs/quickstart.md](https://github.com/Vortx-AI/emem/blob/main/docs/quickstart.md)
|
|
47
|
+
and [docs/agents.md](https://github.com/Vortx-AI/emem/blob/main/docs/agents.md).
|
|
48
|
+
|
|
49
|
+
## What the client does
|
|
50
|
+
|
|
51
|
+
The `Client` class wraps the public REST surface. Each method sends one
|
|
52
|
+
request and returns the parsed JSON without changes. That JSON includes
|
|
53
|
+
the Ed25519-signed receipt and the content-addressed CIDs, so you can cite a
|
|
54
|
+
fact and verify it offline. The module docstring of `ememdev.client` maps
|
|
55
|
+
each endpoint to its method. The full REST surface is the list in
|
|
56
|
+
[`/openapi.json`](https://emem.dev/openapi.json). This README gives no count,
|
|
57
|
+
because a count goes out of date.
|
|
58
|
+
|
|
59
|
+
The client does not wrap endpoints that do not return JSON over HTTP/1.1.
|
|
60
|
+
Examples are `/v1/stream` (SSE), the scene PNG and RGB images, and
|
|
61
|
+
`/v1/attest_cbor`. Call them with `httpx` directly. The module docstring
|
|
62
|
+
lists all of them.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install ememdev
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The PyPI distribution and the import package have the same name, `ememdev`.
|
|
71
|
+
The shorter name `emem` on PyPI belongs to a different project.
|
|
72
|
+
To install from a checkout of this repository:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e sdks/emem-py
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The client needs Python 3.9 or later. Its only runtime dependency is
|
|
79
|
+
[`httpx`](https://www.python-httpx.org/). To sign memory writes or verify
|
|
80
|
+
receipts offline, add the `signing` extra. It installs `blake3` and
|
|
81
|
+
`cryptography`:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install "ememdev[signing]"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Read a fact
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from ememdev import Client
|
|
91
|
+
|
|
92
|
+
with Client() as em:
|
|
93
|
+
cell = em.locate("Mount Fuji")["cell64"]
|
|
94
|
+
facts = em.recall(cell, bands=["copdem30m.elevation_mean"])
|
|
95
|
+
print(facts["facts"][0]["value"])
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
On 7 November 2026 this printed `3539.042236328125` (metres). If no fact
|
|
99
|
+
is in memory for the cell, `recall` fetches the source first. That first
|
|
100
|
+
read can take tens of seconds; the client timeout is 180 seconds.
|
|
101
|
+
|
|
102
|
+
## Use the async client
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import asyncio
|
|
106
|
+
from ememdev import AsyncClient
|
|
107
|
+
|
|
108
|
+
async def main() -> None:
|
|
109
|
+
async with AsyncClient() as em:
|
|
110
|
+
out = await em.ask("How tall is Mount Everest?")
|
|
111
|
+
print(out["answer"])
|
|
112
|
+
|
|
113
|
+
asyncio.run(main())
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`AsyncClient` has the same methods as `Client`.
|
|
117
|
+
|
|
118
|
+
## Configure the client
|
|
119
|
+
|
|
120
|
+
| Environment variable | Default | Effect |
|
|
121
|
+
|----------------------|--------------------|------------------------------------------------|
|
|
122
|
+
| `EMEM_BASE_URL` | `https://emem.dev` | Responder root. Set it to use a self-hosted node. |
|
|
123
|
+
| `EMEM_TIMEOUT_SECS` | `180` | HTTP timeout in seconds. It matches the gateway timeout. |
|
|
124
|
+
|
|
125
|
+
You can also give `base_url=` and `timeout=` to the constructor.
|
|
126
|
+
|
|
127
|
+
## Write a signed note
|
|
128
|
+
|
|
129
|
+
Reads are anonymous. A write into the agent memory namespace must carry an
|
|
130
|
+
Ed25519 `attester` block. The MCP client cannot make that signature for you.
|
|
131
|
+
`ememdev[signing]` makes it from a persistent identity, so one command
|
|
132
|
+
writes a signed note.
|
|
133
|
+
|
|
134
|
+
The key owns your namespace at `/memories/by_attester/<pubkey8>/...`, so
|
|
135
|
+
the client uses the same identity in every session. It looks for the key
|
|
136
|
+
in this order:
|
|
137
|
+
|
|
138
|
+
1. `EMEM_AGENT_KEY`: a 64-character hex seed or the path of a key file.
|
|
139
|
+
2. `~/.emem/agent_ed25519.pem`. Set `EMEM_HOME` to use a different directory.
|
|
140
|
+
3. If there is no key, the client makes one at that path with mode `0600`.
|
|
141
|
+
|
|
142
|
+
From the command line, for an agent that already uses emem over MCP:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
ememdev whoami # your pubkey and namespace_root
|
|
146
|
+
|
|
147
|
+
# write only the attester block to stdout; no network call:
|
|
148
|
+
ememdev sign --verb create --path /memories/by_attester/<you>/note.md --stdin < note.md
|
|
149
|
+
|
|
150
|
+
# sign a create and POST it to /mcp in one step (this is a write to emem.dev):
|
|
151
|
+
ememdev write --path /memories/by_attester/<you>/note.md --body-file note.md
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
In Python:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from ememdev import load_or_create_signer
|
|
158
|
+
|
|
159
|
+
signer = load_or_create_signer() # persistent identity
|
|
160
|
+
path = f"{signer.namespace_root}/note.md"
|
|
161
|
+
body = b"a durable, signed, citeable note"
|
|
162
|
+
attester = signer.attester_block("create", path, body)
|
|
163
|
+
# POST {path, file_text, kind, attester} to /mcp emem_memory_create.
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Each block uses the v2 preimage, which binds `base`: the `file_cid` that is at
|
|
167
|
+
the path now. A `create` with no `base` signs `absent`, for a new path. To
|
|
168
|
+
overwrite a note, pass `base=<its file_cid>` (`ememdev write --base`). The
|
|
169
|
+
other verbs change a note that exists, so they need `base`.
|
|
170
|
+
|
|
171
|
+
For `str_replace` and `insert`, sign the whole file after the edit. For
|
|
172
|
+
`rename`, use `signer.rename_attester_block(old, new)`, which signs both
|
|
173
|
+
paths. The `ememdev.signing` docstring gives the body rule for each verb.
|
|
174
|
+
The server refuses a write into the namespace of a different key with a 403.
|
|
175
|
+
The `sign` and `write` commands refuse it locally before they send anything.
|
|
176
|
+
|
|
177
|
+
## Cite and verify a receipt
|
|
178
|
+
|
|
179
|
+
Every response from a read primitive (for example `recall`) has a `receipt`
|
|
180
|
+
block. Some responses, such as `locate`, have none. A receipt contains these
|
|
181
|
+
fields:
|
|
182
|
+
|
|
183
|
+
- `responder_pubkey_b32`: the Ed25519 public key, base32 without padding, lowercase.
|
|
184
|
+
- `signature_b32`: the Ed25519 signature over the BLAKE3 preimage digest.
|
|
185
|
+
- `preimage_version`: the rule that made the signature. Read it; do not assume it.
|
|
186
|
+
- `merkle_proof`: the inclusion proof for `fact_cids[0]`, if the log recorded one.
|
|
187
|
+
- `fact_cids[]`: the BLAKE3 CIDs of all facts in the response.
|
|
188
|
+
|
|
189
|
+
To cite an answer, quote `receipt.fact_cids[0]` and the responder pubkey.
|
|
190
|
+
Anyone can verify the signature offline. With the `signing` extra installed,
|
|
191
|
+
`ememdev.verify` does the check in your process:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from ememdev import Client
|
|
195
|
+
from ememdev.verify import verify_receipt_offline
|
|
196
|
+
|
|
197
|
+
with Client() as em:
|
|
198
|
+
cell = em.locate("Mount Fuji")["cell64"]
|
|
199
|
+
resp = em.recall(cell, bands=["copdem30m.elevation_mean"])
|
|
200
|
+
v = verify_receipt_offline(resp["receipt"])
|
|
201
|
+
print(v.ok, v.state, v.signer_b32)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
On 7 November 2026 this printed `True verified 777er3yi...` with
|
|
205
|
+
`preimage_version` 2. The function checks the signature against the key in
|
|
206
|
+
the receipt. Compare `v.signer_b32` with the key you trust, for example
|
|
207
|
+
`federation.node.responder_pubkey_b32` in
|
|
208
|
+
`https://emem.dev/.well-known/emem.json`. On 7 November 2026 the two keys
|
|
209
|
+
were the same. The function does not raise and does not use the network.
|
|
210
|
+
If no check ran, `v.ok` is `False` and `v.state` gives the cause.
|
|
211
|
+
Without the extra, the state is `crypto_unavailable`. If the local encoders
|
|
212
|
+
disagree with the Rust test vectors, the state is `self_test_failed`.
|
|
213
|
+
`Client.verify_receipt()` is different: it asks the
|
|
214
|
+
responder to do the check.
|
|
215
|
+
|
|
216
|
+
Keep and forward the receipt whole. `ememdev` returns it without changes;
|
|
217
|
+
store it and pass it on the same way. From `preimage_version: 2`, the
|
|
218
|
+
signature also covers the inclusion proof. Thus a party between you and the
|
|
219
|
+
reader cannot remove the proof without detection.
|
|
220
|
+
|
|
221
|
+
The cost is that a changed receipt fails. Remove `merkle_proof`, rename a
|
|
222
|
+
field or summarise the envelope, and the check gives
|
|
223
|
+
`signature_valid: false`. That result looks the same as a forgery, although
|
|
224
|
+
nobody changed the data. If the responder can prove that someone reshaped
|
|
225
|
+
the receipt, `POST /v1/verify_receipt` gives `reason:
|
|
226
|
+
receipt_reshaped_after_signing` instead of `signature_invalid`. It never
|
|
227
|
+
accepts the changed receipt.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
Apache-2.0, the same license as the protocol.
|
ememdev-2.4.5/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# ememdev: Python client for emem.dev
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.Vortx-AI/emem -->
|
|
4
|
+
|
|
5
|
+
`ememdev` is the Python client for the [emem.dev](https://emem.dev) Earth
|
|
6
|
+
memory protocol. Use it when a Python program or agent must read signed
|
|
7
|
+
Earth-observation facts and keep the receipts for citation. Reads need no key.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install ememdev
|
|
11
|
+
python -c 'from ememdev import Client; print(Client().locate("Mount Fuji")["cell64"])'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The second line prints the cell64 address of Mount Fuji (`defi.zb592.nUkO.zEzE`
|
|
15
|
+
on 7 November 2026). For the protocol itself, read
|
|
16
|
+
[docs/quickstart.md](https://github.com/Vortx-AI/emem/blob/main/docs/quickstart.md)
|
|
17
|
+
and [docs/agents.md](https://github.com/Vortx-AI/emem/blob/main/docs/agents.md).
|
|
18
|
+
|
|
19
|
+
## What the client does
|
|
20
|
+
|
|
21
|
+
The `Client` class wraps the public REST surface. Each method sends one
|
|
22
|
+
request and returns the parsed JSON without changes. That JSON includes
|
|
23
|
+
the Ed25519-signed receipt and the content-addressed CIDs, so you can cite a
|
|
24
|
+
fact and verify it offline. The module docstring of `ememdev.client` maps
|
|
25
|
+
each endpoint to its method. The full REST surface is the list in
|
|
26
|
+
[`/openapi.json`](https://emem.dev/openapi.json). This README gives no count,
|
|
27
|
+
because a count goes out of date.
|
|
28
|
+
|
|
29
|
+
The client does not wrap endpoints that do not return JSON over HTTP/1.1.
|
|
30
|
+
Examples are `/v1/stream` (SSE), the scene PNG and RGB images, and
|
|
31
|
+
`/v1/attest_cbor`. Call them with `httpx` directly. The module docstring
|
|
32
|
+
lists all of them.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ememdev
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The PyPI distribution and the import package have the same name, `ememdev`.
|
|
41
|
+
The shorter name `emem` on PyPI belongs to a different project.
|
|
42
|
+
To install from a checkout of this repository:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install -e sdks/emem-py
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The client needs Python 3.9 or later. Its only runtime dependency is
|
|
49
|
+
[`httpx`](https://www.python-httpx.org/). To sign memory writes or verify
|
|
50
|
+
receipts offline, add the `signing` extra. It installs `blake3` and
|
|
51
|
+
`cryptography`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install "ememdev[signing]"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Read a fact
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from ememdev import Client
|
|
61
|
+
|
|
62
|
+
with Client() as em:
|
|
63
|
+
cell = em.locate("Mount Fuji")["cell64"]
|
|
64
|
+
facts = em.recall(cell, bands=["copdem30m.elevation_mean"])
|
|
65
|
+
print(facts["facts"][0]["value"])
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
On 7 November 2026 this printed `3539.042236328125` (metres). If no fact
|
|
69
|
+
is in memory for the cell, `recall` fetches the source first. That first
|
|
70
|
+
read can take tens of seconds; the client timeout is 180 seconds.
|
|
71
|
+
|
|
72
|
+
## Use the async client
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from ememdev import AsyncClient
|
|
77
|
+
|
|
78
|
+
async def main() -> None:
|
|
79
|
+
async with AsyncClient() as em:
|
|
80
|
+
out = await em.ask("How tall is Mount Everest?")
|
|
81
|
+
print(out["answer"])
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`AsyncClient` has the same methods as `Client`.
|
|
87
|
+
|
|
88
|
+
## Configure the client
|
|
89
|
+
|
|
90
|
+
| Environment variable | Default | Effect |
|
|
91
|
+
|----------------------|--------------------|------------------------------------------------|
|
|
92
|
+
| `EMEM_BASE_URL` | `https://emem.dev` | Responder root. Set it to use a self-hosted node. |
|
|
93
|
+
| `EMEM_TIMEOUT_SECS` | `180` | HTTP timeout in seconds. It matches the gateway timeout. |
|
|
94
|
+
|
|
95
|
+
You can also give `base_url=` and `timeout=` to the constructor.
|
|
96
|
+
|
|
97
|
+
## Write a signed note
|
|
98
|
+
|
|
99
|
+
Reads are anonymous. A write into the agent memory namespace must carry an
|
|
100
|
+
Ed25519 `attester` block. The MCP client cannot make that signature for you.
|
|
101
|
+
`ememdev[signing]` makes it from a persistent identity, so one command
|
|
102
|
+
writes a signed note.
|
|
103
|
+
|
|
104
|
+
The key owns your namespace at `/memories/by_attester/<pubkey8>/...`, so
|
|
105
|
+
the client uses the same identity in every session. It looks for the key
|
|
106
|
+
in this order:
|
|
107
|
+
|
|
108
|
+
1. `EMEM_AGENT_KEY`: a 64-character hex seed or the path of a key file.
|
|
109
|
+
2. `~/.emem/agent_ed25519.pem`. Set `EMEM_HOME` to use a different directory.
|
|
110
|
+
3. If there is no key, the client makes one at that path with mode `0600`.
|
|
111
|
+
|
|
112
|
+
From the command line, for an agent that already uses emem over MCP:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
ememdev whoami # your pubkey and namespace_root
|
|
116
|
+
|
|
117
|
+
# write only the attester block to stdout; no network call:
|
|
118
|
+
ememdev sign --verb create --path /memories/by_attester/<you>/note.md --stdin < note.md
|
|
119
|
+
|
|
120
|
+
# sign a create and POST it to /mcp in one step (this is a write to emem.dev):
|
|
121
|
+
ememdev write --path /memories/by_attester/<you>/note.md --body-file note.md
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
In Python:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from ememdev import load_or_create_signer
|
|
128
|
+
|
|
129
|
+
signer = load_or_create_signer() # persistent identity
|
|
130
|
+
path = f"{signer.namespace_root}/note.md"
|
|
131
|
+
body = b"a durable, signed, citeable note"
|
|
132
|
+
attester = signer.attester_block("create", path, body)
|
|
133
|
+
# POST {path, file_text, kind, attester} to /mcp emem_memory_create.
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Each block uses the v2 preimage, which binds `base`: the `file_cid` that is at
|
|
137
|
+
the path now. A `create` with no `base` signs `absent`, for a new path. To
|
|
138
|
+
overwrite a note, pass `base=<its file_cid>` (`ememdev write --base`). The
|
|
139
|
+
other verbs change a note that exists, so they need `base`.
|
|
140
|
+
|
|
141
|
+
For `str_replace` and `insert`, sign the whole file after the edit. For
|
|
142
|
+
`rename`, use `signer.rename_attester_block(old, new)`, which signs both
|
|
143
|
+
paths. The `ememdev.signing` docstring gives the body rule for each verb.
|
|
144
|
+
The server refuses a write into the namespace of a different key with a 403.
|
|
145
|
+
The `sign` and `write` commands refuse it locally before they send anything.
|
|
146
|
+
|
|
147
|
+
## Cite and verify a receipt
|
|
148
|
+
|
|
149
|
+
Every response from a read primitive (for example `recall`) has a `receipt`
|
|
150
|
+
block. Some responses, such as `locate`, have none. A receipt contains these
|
|
151
|
+
fields:
|
|
152
|
+
|
|
153
|
+
- `responder_pubkey_b32`: the Ed25519 public key, base32 without padding, lowercase.
|
|
154
|
+
- `signature_b32`: the Ed25519 signature over the BLAKE3 preimage digest.
|
|
155
|
+
- `preimage_version`: the rule that made the signature. Read it; do not assume it.
|
|
156
|
+
- `merkle_proof`: the inclusion proof for `fact_cids[0]`, if the log recorded one.
|
|
157
|
+
- `fact_cids[]`: the BLAKE3 CIDs of all facts in the response.
|
|
158
|
+
|
|
159
|
+
To cite an answer, quote `receipt.fact_cids[0]` and the responder pubkey.
|
|
160
|
+
Anyone can verify the signature offline. With the `signing` extra installed,
|
|
161
|
+
`ememdev.verify` does the check in your process:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from ememdev import Client
|
|
165
|
+
from ememdev.verify import verify_receipt_offline
|
|
166
|
+
|
|
167
|
+
with Client() as em:
|
|
168
|
+
cell = em.locate("Mount Fuji")["cell64"]
|
|
169
|
+
resp = em.recall(cell, bands=["copdem30m.elevation_mean"])
|
|
170
|
+
v = verify_receipt_offline(resp["receipt"])
|
|
171
|
+
print(v.ok, v.state, v.signer_b32)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
On 7 November 2026 this printed `True verified 777er3yi...` with
|
|
175
|
+
`preimage_version` 2. The function checks the signature against the key in
|
|
176
|
+
the receipt. Compare `v.signer_b32` with the key you trust, for example
|
|
177
|
+
`federation.node.responder_pubkey_b32` in
|
|
178
|
+
`https://emem.dev/.well-known/emem.json`. On 7 November 2026 the two keys
|
|
179
|
+
were the same. The function does not raise and does not use the network.
|
|
180
|
+
If no check ran, `v.ok` is `False` and `v.state` gives the cause.
|
|
181
|
+
Without the extra, the state is `crypto_unavailable`. If the local encoders
|
|
182
|
+
disagree with the Rust test vectors, the state is `self_test_failed`.
|
|
183
|
+
`Client.verify_receipt()` is different: it asks the
|
|
184
|
+
responder to do the check.
|
|
185
|
+
|
|
186
|
+
Keep and forward the receipt whole. `ememdev` returns it without changes;
|
|
187
|
+
store it and pass it on the same way. From `preimage_version: 2`, the
|
|
188
|
+
signature also covers the inclusion proof. Thus a party between you and the
|
|
189
|
+
reader cannot remove the proof without detection.
|
|
190
|
+
|
|
191
|
+
The cost is that a changed receipt fails. Remove `merkle_proof`, rename a
|
|
192
|
+
field or summarise the envelope, and the check gives
|
|
193
|
+
`signature_valid: false`. That result looks the same as a forgery, although
|
|
194
|
+
nobody changed the data. If the responder can prove that someone reshaped
|
|
195
|
+
the receipt, `POST /v1/verify_receipt` gives `reason:
|
|
196
|
+
receipt_reshaped_after_signing` instead of `signature_invalid`. It never
|
|
197
|
+
accepts the changed receipt.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
Apache-2.0, the same license as the protocol.
|
|
@@ -9,7 +9,7 @@ name = "ememdev"
|
|
|
9
9
|
# without a framework. 1.0.1 first superseded the empty 1.0.0 on PyPI (it
|
|
10
10
|
# shipped dist-info and no modules, so `import emem` raised ModuleNotFoundError;
|
|
11
11
|
# PyPI versions are immutable, so the only fix was to supersede it).
|
|
12
|
-
version = "2.4.
|
|
12
|
+
version = "2.4.5"
|
|
13
13
|
description = "Python client for emem: shared, verifiable memory for AI agents. One address per fact, one signature you can check. No key to read."
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.9"
|
|
@@ -149,10 +149,11 @@ def _build_attester(signer, args: argparse.Namespace) -> dict:
|
|
|
149
149
|
body = _read_body(args)
|
|
150
150
|
if verb == "delete" and body:
|
|
151
151
|
raise SystemExit("delete carries no body; drop --body/--body-file/--stdin")
|
|
152
|
-
if verb
|
|
152
|
+
if verb != "create" and not args.base:
|
|
153
153
|
raise SystemExit(
|
|
154
|
-
"
|
|
155
|
-
"(GET the note with Accept: application/json
|
|
154
|
+
f"{verb} changes a note that exists, so it must be signed against it; pass "
|
|
155
|
+
"--base <file_cid now at --path> (GET the note with Accept: application/json "
|
|
156
|
+
"and read file_cid)"
|
|
156
157
|
)
|
|
157
158
|
if verb in ("create", "str_replace", "insert") and not body and not args.allow_empty:
|
|
158
159
|
raise SystemExit(
|
|
@@ -185,7 +186,7 @@ def _cmd_write(args: argparse.Namespace) -> int:
|
|
|
185
186
|
if not body and not args.allow_empty:
|
|
186
187
|
raise SystemExit("write needs a body; pass --body/--body-file/--stdin or --allow-empty")
|
|
187
188
|
file_text = body.decode("utf-8")
|
|
188
|
-
attester = signer.attester_block("create", args.path, body)
|
|
189
|
+
attester = signer.attester_block("create", args.path, body, base=args.base)
|
|
189
190
|
result = _post_memory_create(args.base_url, args.path, file_text, args.kind, attester)
|
|
190
191
|
print(json.dumps(result, indent=2))
|
|
191
192
|
return 0
|
|
@@ -223,8 +224,8 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
223
224
|
p_sign.add_argument("--old-path", help="rename only: the source path")
|
|
224
225
|
p_sign.add_argument(
|
|
225
226
|
"--base",
|
|
226
|
-
help="the file_cid now at --path,
|
|
227
|
-
"'absent'
|
|
227
|
+
help="the file_cid now at --path. delete, str_replace and insert must name "
|
|
228
|
+
"it; create defaults to 'absent' (a new path)",
|
|
228
229
|
)
|
|
229
230
|
_add_body_args(p_sign)
|
|
230
231
|
|
|
@@ -234,6 +235,10 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
234
235
|
p_write.add_argument("--path", required=True)
|
|
235
236
|
p_write.add_argument("--kind", default="resource")
|
|
236
237
|
p_write.add_argument("--base-url", default="https://emem.dev")
|
|
238
|
+
p_write.add_argument(
|
|
239
|
+
"--base",
|
|
240
|
+
help="to overwrite a note, the file_cid now at --path; omit for a new path",
|
|
241
|
+
)
|
|
237
242
|
_add_body_args(p_write)
|
|
238
243
|
|
|
239
244
|
args = parser.parse_args(argv)
|
|
@@ -4,10 +4,15 @@ The emem responder gates every memory write verb (`create`,
|
|
|
4
4
|
`str_replace`, `insert`, `delete`, `rename`) behind an `attester`
|
|
5
5
|
block. The block binds the write to an ed25519 key:
|
|
6
6
|
|
|
7
|
-
sig = ed25519(blake3(b"emem.memory_write|" + verb + b"|"
|
|
8
|
-
+ path + b"|" + body_hash))
|
|
7
|
+
sig = ed25519(blake3(b"emem.memory_write.v2|" + verb + b"|"
|
|
8
|
+
+ path + b"|" + body_hash + b"|" + base))
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
`base` is the `file_cid` now at `path`, or `absent` when nothing is there.
|
|
11
|
+
The v1 preimage (no `base`) is retired for memory writes: the responder
|
|
12
|
+
refuses it. `attester_preimage` (v1) stays because `/v1/derive` and
|
|
13
|
+
`/v1/entity` still sign under it.
|
|
14
|
+
|
|
15
|
+
The block travels on the wire as::
|
|
11
16
|
|
|
12
17
|
"attester": {"pubkey_b32": "<52 chars>", "sig_b32": "<103 chars>"}
|
|
13
18
|
|
|
@@ -78,8 +83,9 @@ _PREIMAGE_TAG = b"emem.memory_write|"
|
|
|
78
83
|
_PREIMAGE_V2_TAG = b"emem.memory_write.v2|"
|
|
79
84
|
#: What `base` is when nothing is at the path yet (emem_primitives::BASE_ABSENT).
|
|
80
85
|
BASE_ABSENT = "absent"
|
|
81
|
-
#: Verbs the responder refuses to verify under the v1 preimage
|
|
82
|
-
|
|
86
|
+
#: Verbs the responder refuses to verify under the v1 preimage: every
|
|
87
|
+
#: write verb (lib.rs validate_attester_binding, reason v1_retired).
|
|
88
|
+
V2_REQUIRED_VERBS = WRITE_VERBS
|
|
83
89
|
_SEP = b"|"
|
|
84
90
|
|
|
85
91
|
|
|
@@ -238,23 +244,27 @@ class EmemSigner:
|
|
|
238
244
|
def attester_block(
|
|
239
245
|
self, verb: str, path: str, body: bytes = b"", base: Optional[str] = None
|
|
240
246
|
) -> dict:
|
|
241
|
-
"""Build the wire `attester` block for one write
|
|
247
|
+
"""Build the wire `attester` block for one write, signed with the v2
|
|
248
|
+
preimage.
|
|
242
249
|
|
|
243
250
|
`body` is the byte string the responder will hash for this verb
|
|
244
251
|
(see the table in this module's docstring). It defaults to empty,
|
|
245
252
|
which is right for `delete`. For `rename`, prefer
|
|
246
253
|
`rename_attester_block`.
|
|
254
|
+
|
|
255
|
+
`base` is the `file_cid` now at `path`, or `BASE_ABSENT`. A `create`
|
|
256
|
+
without `base` signs `BASE_ABSENT`, which is right for a new note; to
|
|
257
|
+
overwrite a note, pass its current `file_cid`. Every other verb
|
|
258
|
+
changes a note that exists, so it must name the `base`.
|
|
247
259
|
"""
|
|
248
|
-
if base is None
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
else attester_preimage(verb, path, body_hash(body))
|
|
257
|
-
)
|
|
260
|
+
if base is None:
|
|
261
|
+
if verb != "create":
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"{verb} must be signed against base= the file_cid now at the path "
|
|
264
|
+
"(for rename, use rename_attester_block)"
|
|
265
|
+
)
|
|
266
|
+
base = BASE_ABSENT
|
|
267
|
+
digest = attester_preimage_v2(verb, path, body_hash(body), base)
|
|
258
268
|
return {
|
|
259
269
|
"pubkey_b32": self.pubkey_b32,
|
|
260
270
|
"sig_b32": b32_nopad_lc(self.sign_preimage(digest)),
|
ememdev-2.4.2/PKG-INFO
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: ememdev
|
|
3
|
-
Version: 2.4.2
|
|
4
|
-
Summary: Python client for emem: shared, verifiable memory for AI agents. One address per fact, one signature you can check. No key to read.
|
|
5
|
-
Project-URL: Homepage, https://emem.dev
|
|
6
|
-
Project-URL: Repository, https://github.com/Vortx-AI/emem
|
|
7
|
-
Project-URL: Documentation, https://emem.dev/agents.md
|
|
8
|
-
Project-URL: Issues, https://github.com/Vortx-AI/emem/issues
|
|
9
|
-
Author-email: Vortx AI Private Limited <avijeet@vortx.ai>
|
|
10
|
-
License: Apache-2.0
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Keywords: agent-memory,ai-agents,content-addressed,earth-observation,ed25519,geospatial,mcp,model-context-protocol,provenance,satellite,shared-memory,verifiable
|
|
13
|
-
Classifier: Development Status :: 3 - Alpha
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Intended Audience :: Science/Research
|
|
16
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
24
|
-
Requires-Python: >=3.9
|
|
25
|
-
Requires-Dist: httpx<1.0,>=0.27
|
|
26
|
-
Provides-Extra: signing
|
|
27
|
-
Requires-Dist: blake3>=0.3; extra == 'signing'
|
|
28
|
-
Requires-Dist: cryptography>=41; extra == 'signing'
|
|
29
|
-
Description-Content-Type: text/markdown
|
|
30
|
-
|
|
31
|
-
# ememdev: Python client for emem.dev
|
|
32
|
-
|
|
33
|
-
<!-- mcp-name: io.github.Vortx-AI/emem -->
|
|
34
|
-
|
|
35
|
-
Thin, typed Python client for the [emem.dev](https://emem.dev) Earth memory
|
|
36
|
-
protocol. Wraps the public REST surface in a single `Client` class that
|
|
37
|
-
returns parsed JSON verbatim. Every ed25519-signed receipt and
|
|
38
|
-
content-addressed CID is preserved for citation and offline verification.
|
|
39
|
-
The current surface is whatever [`/openapi.json`](https://emem.dev/openapi.json)
|
|
40
|
-
lists; this README does not restate a count that would go stale.
|
|
41
|
-
|
|
42
|
-
## Install
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
pip install ememdev
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
The distribution and the import package are both named `ememdev` (so
|
|
49
|
-
`pip install ememdev`, then `from ememdev import Client`); the shorter name
|
|
50
|
-
`emem` on PyPI belongs to an unrelated project. To work from a checkout
|
|
51
|
-
instead:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
pip install -e sdks/emem-py
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Requires Python 3.9+. The only runtime dependency is
|
|
58
|
-
[`httpx`](https://www.python-httpx.org/). To sign memory writes (below),
|
|
59
|
-
add the `signing` extra, which pulls `blake3` and `cryptography`:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
pip install "ememdev[signing]"
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Quick start
|
|
66
|
-
|
|
67
|
-
```python
|
|
68
|
-
from ememdev import Client
|
|
69
|
-
|
|
70
|
-
with Client() as em:
|
|
71
|
-
cell = em.locate("Mount Fuji")["cell64"]
|
|
72
|
-
facts = em.recall(cell, bands=["copdem30m.elevation_mean"])
|
|
73
|
-
print(facts["facts"][0]["value"])
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Async
|
|
77
|
-
|
|
78
|
-
```python
|
|
79
|
-
import asyncio
|
|
80
|
-
from ememdev import AsyncClient
|
|
81
|
-
|
|
82
|
-
async def main() -> None:
|
|
83
|
-
async with AsyncClient() as em:
|
|
84
|
-
out = await em.ask("How tall is Mount Everest?")
|
|
85
|
-
print(out["answer"])
|
|
86
|
-
|
|
87
|
-
asyncio.run(main())
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Configuration
|
|
91
|
-
|
|
92
|
-
| Env var | Default | Effect |
|
|
93
|
-
|-----------------------|----------------------|---------------------------------------------|
|
|
94
|
-
| `EMEM_BASE_URL` | `https://emem.dev` | Responder root; point at a self-hosted node |
|
|
95
|
-
| `EMEM_TIMEOUT_SECS` | `180` | HTTP timeout (matches gateway timeout) |
|
|
96
|
-
|
|
97
|
-
You can also pass `base_url=` and `timeout=` directly to the constructor.
|
|
98
|
-
|
|
99
|
-
## Surface coverage
|
|
100
|
-
|
|
101
|
-
Geocoder + read primitives, physics solvers, boring lat/lng shortcuts, and
|
|
102
|
-
introspection — see the inline docstring on `emem.client` for the full
|
|
103
|
-
endpoint → method mapping.
|
|
104
|
-
|
|
105
|
-
## Writing a signed note
|
|
106
|
-
|
|
107
|
-
Reads are anonymous. Writes into the agent memory namespace are gated by an
|
|
108
|
-
ed25519 `attester` block, and that signature is the one thing the MCP client
|
|
109
|
-
cannot produce for you. `ememdev[signing]` produces it, from a persistent
|
|
110
|
-
identity, so leaving a signed note is one command rather than a hand-rolled
|
|
111
|
-
crypto script.
|
|
112
|
-
|
|
113
|
-
The identity is resolved once and reused every session, because the key is
|
|
114
|
-
what owns your namespace at `/memories/by_attester/<pubkey8>/...`. Resolution
|
|
115
|
-
order: `EMEM_AGENT_KEY` (a 64-char hex seed or a key-file path), then
|
|
116
|
-
`~/.emem/agent_ed25519.pem` (override the directory with `EMEM_HOME`), minted
|
|
117
|
-
`0600` on first use if absent.
|
|
118
|
-
|
|
119
|
-
From the command line, for an agent that already speaks to emem over MCP:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
ememdev whoami # your pubkey and namespace_root
|
|
123
|
-
|
|
124
|
-
# emit just the attester block, sign nothing else (no network):
|
|
125
|
-
ememdev sign --verb create --path /memories/by_attester/<you>/note.md --stdin < note.md
|
|
126
|
-
|
|
127
|
-
# or sign a create and POST it to /mcp in one step:
|
|
128
|
-
ememdev write --path /memories/by_attester/<you>/note.md --body-file note.md
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Or in Python:
|
|
132
|
-
|
|
133
|
-
```python
|
|
134
|
-
from ememdev import load_or_create_signer
|
|
135
|
-
|
|
136
|
-
signer = load_or_create_signer() # persistent identity
|
|
137
|
-
path = f"{signer.namespace_root}/note.md"
|
|
138
|
-
body = b"a durable, signed, citeable note"
|
|
139
|
-
attester = signer.attester_block("create", path, body)
|
|
140
|
-
# POST {path, file_text, kind, attester} to /mcp emem_memory_create.
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
`str_replace` and `insert` sign the whole file *after* the edit, and `rename`
|
|
144
|
-
binds both ends of the move (`signer.rename_attester_block(old, new)`); see the
|
|
145
|
-
`ememdev.signing` docstring for the per-verb body rule. A write into another
|
|
146
|
-
key's namespace is refused with a 403, and `sign`/`write` refuse it locally
|
|
147
|
-
first.
|
|
148
|
-
|
|
149
|
-
## Receipts
|
|
150
|
-
|
|
151
|
-
Every non-introspection response carries a `receipt` block with:
|
|
152
|
-
|
|
153
|
-
- `responder_pubkey_b32` (ed25519 base32-nopad-lowercase)
|
|
154
|
-
- `signature_b32` (ed25519 over the BLAKE3 preimage digest)
|
|
155
|
-
- `preimage_version` (which rule signed it; read it, do not assume)
|
|
156
|
-
- `merkle_proof` (inclusion proof for `fact_cids[0]`, when one was recorded)
|
|
157
|
-
- `fact_cids[]` (the BLAKE3 CIDs of every fact returned)
|
|
158
|
-
|
|
159
|
-
To cite an answer: quote `receipt.fact_cids[0]` and the responder pubkey.
|
|
160
|
-
The signature can be verified offline against the public key at
|
|
161
|
-
`https://emem.dev/.well-known/emem.json`; no callback to the responder is
|
|
162
|
-
required.
|
|
163
|
-
|
|
164
|
-
**Pass the receipt on whole.** `ememdev` returns it verbatim and you should
|
|
165
|
-
store and forward it the same way. From `preimage_version: 2` the signature
|
|
166
|
-
covers the inclusion proof as well as every other field it binds, which is
|
|
167
|
-
what stops an intermediary stripping the proof in transit. The cost is that
|
|
168
|
-
a receipt is byte-for-byte or nothing: drop `merkle_proof` because it looks
|
|
169
|
-
redundant, re-key a field, or summarise the envelope, and it comes back
|
|
170
|
-
`signature_valid: false` on data nobody tampered with — indistinguishable
|
|
171
|
-
from forgery. `POST /v1/verify_receipt` reports `reason:
|
|
172
|
-
receipt_reshaped_after_signing` instead of `signature_invalid` when the
|
|
173
|
-
responder can prove that is what happened, but it never accepts the receipt.
|
|
174
|
-
|
|
175
|
-
## License
|
|
176
|
-
|
|
177
|
-
Apache-2.0. Same as the upstream protocol.
|
ememdev-2.4.2/README.md
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
# ememdev: Python client for emem.dev
|
|
2
|
-
|
|
3
|
-
<!-- mcp-name: io.github.Vortx-AI/emem -->
|
|
4
|
-
|
|
5
|
-
Thin, typed Python client for the [emem.dev](https://emem.dev) Earth memory
|
|
6
|
-
protocol. Wraps the public REST surface in a single `Client` class that
|
|
7
|
-
returns parsed JSON verbatim. Every ed25519-signed receipt and
|
|
8
|
-
content-addressed CID is preserved for citation and offline verification.
|
|
9
|
-
The current surface is whatever [`/openapi.json`](https://emem.dev/openapi.json)
|
|
10
|
-
lists; this README does not restate a count that would go stale.
|
|
11
|
-
|
|
12
|
-
## Install
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
pip install ememdev
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
The distribution and the import package are both named `ememdev` (so
|
|
19
|
-
`pip install ememdev`, then `from ememdev import Client`); the shorter name
|
|
20
|
-
`emem` on PyPI belongs to an unrelated project. To work from a checkout
|
|
21
|
-
instead:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
pip install -e sdks/emem-py
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Requires Python 3.9+. The only runtime dependency is
|
|
28
|
-
[`httpx`](https://www.python-httpx.org/). To sign memory writes (below),
|
|
29
|
-
add the `signing` extra, which pulls `blake3` and `cryptography`:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
pip install "ememdev[signing]"
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Quick start
|
|
36
|
-
|
|
37
|
-
```python
|
|
38
|
-
from ememdev import Client
|
|
39
|
-
|
|
40
|
-
with Client() as em:
|
|
41
|
-
cell = em.locate("Mount Fuji")["cell64"]
|
|
42
|
-
facts = em.recall(cell, bands=["copdem30m.elevation_mean"])
|
|
43
|
-
print(facts["facts"][0]["value"])
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Async
|
|
47
|
-
|
|
48
|
-
```python
|
|
49
|
-
import asyncio
|
|
50
|
-
from ememdev import AsyncClient
|
|
51
|
-
|
|
52
|
-
async def main() -> None:
|
|
53
|
-
async with AsyncClient() as em:
|
|
54
|
-
out = await em.ask("How tall is Mount Everest?")
|
|
55
|
-
print(out["answer"])
|
|
56
|
-
|
|
57
|
-
asyncio.run(main())
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Configuration
|
|
61
|
-
|
|
62
|
-
| Env var | Default | Effect |
|
|
63
|
-
|-----------------------|----------------------|---------------------------------------------|
|
|
64
|
-
| `EMEM_BASE_URL` | `https://emem.dev` | Responder root; point at a self-hosted node |
|
|
65
|
-
| `EMEM_TIMEOUT_SECS` | `180` | HTTP timeout (matches gateway timeout) |
|
|
66
|
-
|
|
67
|
-
You can also pass `base_url=` and `timeout=` directly to the constructor.
|
|
68
|
-
|
|
69
|
-
## Surface coverage
|
|
70
|
-
|
|
71
|
-
Geocoder + read primitives, physics solvers, boring lat/lng shortcuts, and
|
|
72
|
-
introspection — see the inline docstring on `emem.client` for the full
|
|
73
|
-
endpoint → method mapping.
|
|
74
|
-
|
|
75
|
-
## Writing a signed note
|
|
76
|
-
|
|
77
|
-
Reads are anonymous. Writes into the agent memory namespace are gated by an
|
|
78
|
-
ed25519 `attester` block, and that signature is the one thing the MCP client
|
|
79
|
-
cannot produce for you. `ememdev[signing]` produces it, from a persistent
|
|
80
|
-
identity, so leaving a signed note is one command rather than a hand-rolled
|
|
81
|
-
crypto script.
|
|
82
|
-
|
|
83
|
-
The identity is resolved once and reused every session, because the key is
|
|
84
|
-
what owns your namespace at `/memories/by_attester/<pubkey8>/...`. Resolution
|
|
85
|
-
order: `EMEM_AGENT_KEY` (a 64-char hex seed or a key-file path), then
|
|
86
|
-
`~/.emem/agent_ed25519.pem` (override the directory with `EMEM_HOME`), minted
|
|
87
|
-
`0600` on first use if absent.
|
|
88
|
-
|
|
89
|
-
From the command line, for an agent that already speaks to emem over MCP:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
ememdev whoami # your pubkey and namespace_root
|
|
93
|
-
|
|
94
|
-
# emit just the attester block, sign nothing else (no network):
|
|
95
|
-
ememdev sign --verb create --path /memories/by_attester/<you>/note.md --stdin < note.md
|
|
96
|
-
|
|
97
|
-
# or sign a create and POST it to /mcp in one step:
|
|
98
|
-
ememdev write --path /memories/by_attester/<you>/note.md --body-file note.md
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Or in Python:
|
|
102
|
-
|
|
103
|
-
```python
|
|
104
|
-
from ememdev import load_or_create_signer
|
|
105
|
-
|
|
106
|
-
signer = load_or_create_signer() # persistent identity
|
|
107
|
-
path = f"{signer.namespace_root}/note.md"
|
|
108
|
-
body = b"a durable, signed, citeable note"
|
|
109
|
-
attester = signer.attester_block("create", path, body)
|
|
110
|
-
# POST {path, file_text, kind, attester} to /mcp emem_memory_create.
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
`str_replace` and `insert` sign the whole file *after* the edit, and `rename`
|
|
114
|
-
binds both ends of the move (`signer.rename_attester_block(old, new)`); see the
|
|
115
|
-
`ememdev.signing` docstring for the per-verb body rule. A write into another
|
|
116
|
-
key's namespace is refused with a 403, and `sign`/`write` refuse it locally
|
|
117
|
-
first.
|
|
118
|
-
|
|
119
|
-
## Receipts
|
|
120
|
-
|
|
121
|
-
Every non-introspection response carries a `receipt` block with:
|
|
122
|
-
|
|
123
|
-
- `responder_pubkey_b32` (ed25519 base32-nopad-lowercase)
|
|
124
|
-
- `signature_b32` (ed25519 over the BLAKE3 preimage digest)
|
|
125
|
-
- `preimage_version` (which rule signed it; read it, do not assume)
|
|
126
|
-
- `merkle_proof` (inclusion proof for `fact_cids[0]`, when one was recorded)
|
|
127
|
-
- `fact_cids[]` (the BLAKE3 CIDs of every fact returned)
|
|
128
|
-
|
|
129
|
-
To cite an answer: quote `receipt.fact_cids[0]` and the responder pubkey.
|
|
130
|
-
The signature can be verified offline against the public key at
|
|
131
|
-
`https://emem.dev/.well-known/emem.json`; no callback to the responder is
|
|
132
|
-
required.
|
|
133
|
-
|
|
134
|
-
**Pass the receipt on whole.** `ememdev` returns it verbatim and you should
|
|
135
|
-
store and forward it the same way. From `preimage_version: 2` the signature
|
|
136
|
-
covers the inclusion proof as well as every other field it binds, which is
|
|
137
|
-
what stops an intermediary stripping the proof in transit. The cost is that
|
|
138
|
-
a receipt is byte-for-byte or nothing: drop `merkle_proof` because it looks
|
|
139
|
-
redundant, re-key a field, or summarise the envelope, and it comes back
|
|
140
|
-
`signature_valid: false` on data nobody tampered with — indistinguishable
|
|
141
|
-
from forgery. `POST /v1/verify_receipt` reports `reason:
|
|
142
|
-
receipt_reshaped_after_signing` instead of `signature_invalid` when the
|
|
143
|
-
responder can prove that is what happened, but it never accepts the receipt.
|
|
144
|
-
|
|
145
|
-
## License
|
|
146
|
-
|
|
147
|
-
Apache-2.0. Same as the upstream protocol.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|