ememdev 2.4.4__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.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.
@@ -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.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 == "delete" and not args.base:
152
+ if verb != "create" and not args.base:
153
153
  raise SystemExit(
154
- "delete is refused under the v1 preimage; pass --base <file_cid now at --path> "
155
- "(GET the note with Accept: application/json and read file_cid)"
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, which a delete must name (signs v2); "
227
- "'absent' for a new path",
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
- and travels on the wire as::
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
- V2_REQUIRED_VERBS = ("delete", "rename")
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 and verb in V2_REQUIRED_VERBS:
249
- raise ValueError(
250
- f"{verb} is refused under the v1 preimage; pass base= the file_cid "
251
- "now at the path (for rename, use rename_attester_block)"
252
- )
253
- digest = (
254
- attester_preimage_v2(verb, path, body_hash(body), base)
255
- if base is not None
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.4/PKG-INFO DELETED
@@ -1,177 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: ememdev
3
- Version: 2.4.4
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.4/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
File without changes