ememdev 2.4.0__tar.gz → 2.4.4__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.0 → ememdev-2.4.4}/.gitignore +5 -0
- {ememdev-2.4.0 → ememdev-2.4.4}/PKG-INFO +1 -1
- {ememdev-2.4.0 → ememdev-2.4.4}/pyproject.toml +1 -1
- {ememdev-2.4.0 → ememdev-2.4.4}/src/ememdev/__main__.py +11 -1
- {ememdev-2.4.0 → ememdev-2.4.4}/src/ememdev/client.py +0 -16
- {ememdev-2.4.0 → ememdev-2.4.4}/src/ememdev/signing.py +49 -4
- {ememdev-2.4.0 → ememdev-2.4.4}/LICENSE +0 -0
- {ememdev-2.4.0 → ememdev-2.4.4}/README.md +0 -0
- {ememdev-2.4.0 → ememdev-2.4.4}/src/ememdev/__init__.py +0 -0
- {ememdev-2.4.0 → ememdev-2.4.4}/src/ememdev/identity.py +0 -0
- {ememdev-2.4.0 → ememdev-2.4.4}/src/ememdev/verify.py +0 -0
|
@@ -89,3 +89,8 @@ examples/3d-worlds/out/
|
|
|
89
89
|
|
|
90
90
|
# Agent scratch notes belong in .well-known/agent-notes/, never the repo root.
|
|
91
91
|
/AGENT_*.md
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ememdev
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.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
5
|
Project-URL: Homepage, https://emem.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/Vortx-AI/emem
|
|
@@ -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.4"
|
|
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,12 +149,17 @@ 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:
|
|
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)"
|
|
156
|
+
)
|
|
152
157
|
if verb in ("create", "str_replace", "insert") and not body and not args.allow_empty:
|
|
153
158
|
raise SystemExit(
|
|
154
159
|
f"{verb} needs a body (the whole post-edit file for str_replace/insert). "
|
|
155
160
|
"Pass --body/--body-file/--stdin, or --allow-empty to sign an empty body."
|
|
156
161
|
)
|
|
157
|
-
return signer.attester_block(verb, args.path, body)
|
|
162
|
+
return signer.attester_block(verb, args.path, body, base=args.base)
|
|
158
163
|
|
|
159
164
|
|
|
160
165
|
def _cmd_sign(args: argparse.Namespace) -> int:
|
|
@@ -216,6 +221,11 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
216
221
|
)
|
|
217
222
|
p_sign.add_argument("--path", required=True, help="the memory path being written")
|
|
218
223
|
p_sign.add_argument("--old-path", help="rename only: the source path")
|
|
224
|
+
p_sign.add_argument(
|
|
225
|
+
"--base",
|
|
226
|
+
help="the file_cid now at --path, which a delete must name (signs v2); "
|
|
227
|
+
"'absent' for a new path",
|
|
228
|
+
)
|
|
219
229
|
_add_body_args(p_sign)
|
|
220
230
|
|
|
221
231
|
p_write = sub.add_parser(
|
|
@@ -290,11 +290,6 @@ class Client:
|
|
|
290
290
|
_jepa_predict_body(cell, band, lookback_months, forecast_horizon_months),
|
|
291
291
|
)
|
|
292
292
|
|
|
293
|
-
def jepa_predict_v2(self, cell: str, *, band: str = "indices.ndvi", k_history: int = 5) -> Any:
|
|
294
|
-
return self._post(
|
|
295
|
-
"/v1/jepa_predict_v2", _jepa_predict_v2_body(cell, band, k_history)
|
|
296
|
-
)
|
|
297
|
-
|
|
298
293
|
# ── Boring lat/lng shortcuts ───────────────────────────────────────
|
|
299
294
|
def _boring_get(self, path: str, *, lat: float | None, lng: float | None, place: str | None) -> Any:
|
|
300
295
|
return self._get(path, _boring_get_params(lat, lng, place))
|
|
@@ -764,10 +759,6 @@ def _jepa_predict_body(
|
|
|
764
759
|
}
|
|
765
760
|
|
|
766
761
|
|
|
767
|
-
def _jepa_predict_v2_body(cell: str, band: str, k_history: int) -> dict[str, Any]:
|
|
768
|
-
return {"cell": cell, "band": band, "k_history": k_history}
|
|
769
|
-
|
|
770
|
-
|
|
771
762
|
def _boring_get_params(
|
|
772
763
|
lat: float | None, lng: float | None, place: str | None
|
|
773
764
|
) -> dict[str, Any]:
|
|
@@ -1118,13 +1109,6 @@ class AsyncClient:
|
|
|
1118
1109
|
_jepa_predict_body(cell, band, lookback_months, forecast_horizon_months),
|
|
1119
1110
|
)
|
|
1120
1111
|
|
|
1121
|
-
async def jepa_predict_v2(
|
|
1122
|
-
self, cell: str, *, band: str = "indices.ndvi", k_history: int = 5
|
|
1123
|
-
) -> Any:
|
|
1124
|
-
return await self._post(
|
|
1125
|
-
"/v1/jepa_predict_v2", _jepa_predict_v2_body(cell, band, k_history)
|
|
1126
|
-
)
|
|
1127
|
-
|
|
1128
1112
|
# ── Boring lat/lng shortcuts ───────────────────────────────────────
|
|
1129
1113
|
async def _boring_get(
|
|
1130
1114
|
self, path: str, *, lat: float | None, lng: float | None, place: str | None
|
|
@@ -54,7 +54,7 @@ another key's namespace is refused with a 403.
|
|
|
54
54
|
from __future__ import annotations
|
|
55
55
|
|
|
56
56
|
import base64
|
|
57
|
-
from typing import Union
|
|
57
|
+
from typing import Union, Optional
|
|
58
58
|
|
|
59
59
|
try:
|
|
60
60
|
from blake3 import blake3
|
|
@@ -75,6 +75,11 @@ PUBKEY_SHORT_LEN = 8
|
|
|
75
75
|
WRITE_VERBS = ("create", "str_replace", "insert", "delete", "rename")
|
|
76
76
|
|
|
77
77
|
_PREIMAGE_TAG = b"emem.memory_write|"
|
|
78
|
+
_PREIMAGE_V2_TAG = b"emem.memory_write.v2|"
|
|
79
|
+
#: What `base` is when nothing is at the path yet (emem_primitives::BASE_ABSENT).
|
|
80
|
+
BASE_ABSENT = "absent"
|
|
81
|
+
#: Verbs the responder refuses to verify under the v1 preimage.
|
|
82
|
+
V2_REQUIRED_VERBS = ("delete", "rename")
|
|
78
83
|
_SEP = b"|"
|
|
79
84
|
|
|
80
85
|
|
|
@@ -123,6 +128,33 @@ def attester_preimage(verb: str, path: str, body_digest: bytes) -> bytes:
|
|
|
123
128
|
return h.digest()
|
|
124
129
|
|
|
125
130
|
|
|
131
|
+
def attester_preimage_v2(verb: str, path: str, body_digest: bytes, base: str) -> bytes:
|
|
132
|
+
"""The v2 digest, which the responder tries first and requires for
|
|
133
|
+
delete and rename.
|
|
134
|
+
|
|
135
|
+
`base` is the `file_cid` now at `path`, or `BASE_ABSENT` when nothing is
|
|
136
|
+
there. It binds the signature to the version being replaced, so a
|
|
137
|
+
signature read off the public log cannot be replayed once the path moves
|
|
138
|
+
on. Mirrors emem_primitives::attester_preimage_v2 byte for byte.
|
|
139
|
+
"""
|
|
140
|
+
if verb not in WRITE_VERBS:
|
|
141
|
+
raise ValueError(f"unknown write verb {verb!r}; expected one of {', '.join(WRITE_VERBS)}")
|
|
142
|
+
if len(body_digest) != 32:
|
|
143
|
+
raise ValueError(f"body_digest must be 32 bytes, got {len(body_digest)}")
|
|
144
|
+
if not base:
|
|
145
|
+
raise ValueError("base must be the current file_cid or BASE_ABSENT")
|
|
146
|
+
h = blake3()
|
|
147
|
+
h.update(_PREIMAGE_V2_TAG)
|
|
148
|
+
h.update(verb.encode("utf-8"))
|
|
149
|
+
h.update(_SEP)
|
|
150
|
+
h.update(path.encode("utf-8"))
|
|
151
|
+
h.update(_SEP)
|
|
152
|
+
h.update(body_digest)
|
|
153
|
+
h.update(_SEP)
|
|
154
|
+
h.update(base.encode("utf-8"))
|
|
155
|
+
return h.digest()
|
|
156
|
+
|
|
157
|
+
|
|
126
158
|
def pubkey_short_from_b32(pubkey_b32: str) -> str:
|
|
127
159
|
"""First 8 chars of the lowercased base32 pubkey: the segment the
|
|
128
160
|
responder matches against `/memories/by_attester/<pubkey8>/...`."""
|
|
@@ -203,7 +235,9 @@ class EmemSigner:
|
|
|
203
235
|
"""Raw ed25519 signature (64 bytes) over an already-built preimage."""
|
|
204
236
|
return self._key.sign(preimage)
|
|
205
237
|
|
|
206
|
-
def attester_block(
|
|
238
|
+
def attester_block(
|
|
239
|
+
self, verb: str, path: str, body: bytes = b"", base: Optional[str] = None
|
|
240
|
+
) -> dict:
|
|
207
241
|
"""Build the wire `attester` block for one write.
|
|
208
242
|
|
|
209
243
|
`body` is the byte string the responder will hash for this verb
|
|
@@ -211,7 +245,16 @@ class EmemSigner:
|
|
|
211
245
|
which is right for `delete`. For `rename`, prefer
|
|
212
246
|
`rename_attester_block`.
|
|
213
247
|
"""
|
|
214
|
-
|
|
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
|
+
)
|
|
215
258
|
return {
|
|
216
259
|
"pubkey_b32": self.pubkey_b32,
|
|
217
260
|
"sig_b32": b32_nopad_lc(self.sign_preimage(digest)),
|
|
@@ -233,7 +276,9 @@ class EmemSigner:
|
|
|
233
276
|
"pubkey_b32": self.pubkey_b32,
|
|
234
277
|
"sig_b32": b32_nopad_lc(
|
|
235
278
|
self.sign_preimage(
|
|
236
|
-
|
|
279
|
+
attester_preimage_v2(
|
|
280
|
+
"rename", new_path, rename_body_hash(old_path), BASE_ABSENT
|
|
281
|
+
)
|
|
237
282
|
)
|
|
238
283
|
),
|
|
239
284
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|