ememdev 2.4.0__tar.gz → 2.4.2__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.
@@ -89,3 +89,4 @@ 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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ememdev
3
- Version: 2.4.0
3
+ Version: 2.4.2
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.0"
12
+ version = "2.4.2"
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(self, verb: str, path: str, body: bytes = b"") -> dict:
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
- digest = attester_preimage(verb, path, body_hash(body))
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
- attester_preimage("rename", new_path, rename_body_hash(old_path))
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