attested-memory 0.1.0__py3-none-any.whl
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.
- attested_memory/__init__.py +9 -0
- attested_memory/actor.py +96 -0
- attested_memory/cli.py +152 -0
- attested_memory/client.py +119 -0
- attested_memory-0.1.0.dist-info/METADATA +60 -0
- attested_memory-0.1.0.dist-info/RECORD +9 -0
- attested_memory-0.1.0.dist-info/WHEEL +4 -0
- attested_memory-0.1.0.dist-info/entry_points.txt +2 -0
- attested_memory-0.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Python client for Attested Memory — verifiable memory for AI agents."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .actor import Actor # noqa: E402
|
|
6
|
+
from .client import AttestedMemoryError, Client, IntegrityError, check_integrity, content_hash # noqa: E402
|
|
7
|
+
|
|
8
|
+
__all__ = ["Actor", "AttestedMemoryError", "Client", "IntegrityError", "check_integrity", "content_hash",
|
|
9
|
+
"__version__"]
|
attested_memory/actor.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""The signed actor identity every protected Attested Memory call carries.
|
|
2
|
+
|
|
3
|
+
Wire format (the same in the Memory Market, the SaaS gateway and the product shells):
|
|
4
|
+
|
|
5
|
+
* ``X-Actor-ID`` ``did:actor:`` + hex SHA-256 of the raw Ed25519 public key (74 chars)
|
|
6
|
+
* ``X-Actor-Public-Key`` the raw 32-byte public key, base64url
|
|
7
|
+
* ``X-Actor-Timestamp`` unix seconds; the server accepts five minutes of skew
|
|
8
|
+
* ``X-Actor-Nonce`` 16-64 url-safe characters, good for exactly one request
|
|
9
|
+
* ``X-Actor-Signature`` Ed25519 over ``<actor id>\\n<timestamp>\\n<nonce>``, base64url
|
|
10
|
+
|
|
11
|
+
A captured header set therefore replays nowhere: it is bound to one nonce and expires.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import secrets
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from cryptography.hazmat.primitives import serialization
|
|
25
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _b64url(raw: bytes) -> str:
|
|
29
|
+
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _from_b64url(text: str) -> bytes:
|
|
33
|
+
return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Actor:
|
|
37
|
+
"""An Ed25519 identity. The private key never leaves this object or its key file."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, private_key: Ed25519PrivateKey) -> None:
|
|
40
|
+
self._key = private_key
|
|
41
|
+
self._public = private_key.public_key().public_bytes(
|
|
42
|
+
serialization.Encoding.Raw, serialization.PublicFormat.Raw
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def generate(cls) -> "Actor":
|
|
47
|
+
return cls(Ed25519PrivateKey.generate())
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_seed(cls, seed_b64url: str) -> "Actor":
|
|
51
|
+
seed = _from_b64url(seed_b64url)
|
|
52
|
+
if len(seed) != 32:
|
|
53
|
+
raise ValueError("an actor seed is 32 bytes")
|
|
54
|
+
return cls(Ed25519PrivateKey.from_private_bytes(seed))
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def load(cls, path: str | os.PathLike[str]) -> "Actor":
|
|
58
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
59
|
+
actor = cls.from_seed(data["seed"])
|
|
60
|
+
if data.get("actor_id") and data["actor_id"] != actor.actor_id:
|
|
61
|
+
raise ValueError(f"{path}: actor_id does not match the stored key")
|
|
62
|
+
return actor
|
|
63
|
+
|
|
64
|
+
def save(self, path: str | os.PathLike[str]) -> Path:
|
|
65
|
+
"""Write the key file with mode 0600; refuses to overwrite an existing one."""
|
|
66
|
+
target = Path(path)
|
|
67
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
seed = self._key.private_bytes(
|
|
69
|
+
serialization.Encoding.Raw, serialization.PrivateFormat.Raw, serialization.NoEncryption()
|
|
70
|
+
)
|
|
71
|
+
body = json.dumps({"actor_id": self.actor_id, "seed": _b64url(seed)}, indent=2) + "\n"
|
|
72
|
+
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
73
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
74
|
+
stream.write(body)
|
|
75
|
+
return target
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def public_key(self) -> str:
|
|
79
|
+
return _b64url(self._public)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def actor_id(self) -> str:
|
|
83
|
+
return "did:actor:" + hashlib.sha256(self._public).hexdigest()
|
|
84
|
+
|
|
85
|
+
def proof_headers(self, *, now: float | None = None, nonce: str | None = None) -> dict[str, str]:
|
|
86
|
+
"""Fresh headers for ONE request (a new nonce each call)."""
|
|
87
|
+
timestamp = str(int(time.time() if now is None else now))
|
|
88
|
+
nonce = nonce or secrets.token_urlsafe(24)
|
|
89
|
+
message = f"{self.actor_id}\n{timestamp}\n{nonce}".encode()
|
|
90
|
+
return {
|
|
91
|
+
"X-Actor-ID": self.actor_id,
|
|
92
|
+
"X-Actor-Public-Key": self.public_key,
|
|
93
|
+
"X-Actor-Timestamp": timestamp,
|
|
94
|
+
"X-Actor-Nonce": nonce,
|
|
95
|
+
"X-Actor-Signature": _b64url(self._key.sign(message)),
|
|
96
|
+
}
|
attested_memory/cli.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""attested-memory — command line for the Attested Memory API.
|
|
2
|
+
|
|
3
|
+
attested-memory init # create your actor key (~/.config/attested-memory)
|
|
4
|
+
attested-memory trial # claim the 7-day personal key for that actor
|
|
5
|
+
attested-memory write "Title" "Text" # or: --file notes.md
|
|
6
|
+
attested-memory search "supplier"
|
|
7
|
+
attested-memory read mem_…
|
|
8
|
+
|
|
9
|
+
Environment: ATTESTED_MEMORY_HOME (config dir), ATTESTED_MEMORY_URL (default
|
|
10
|
+
https://attestedmemory.net), ATTESTED_MEMORY_API_KEY (instead of the stored trial key).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from . import __version__
|
|
23
|
+
from .actor import Actor
|
|
24
|
+
from .client import DEFAULT_BASE_URL, AttestedMemoryError, Client
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _home() -> Path:
|
|
28
|
+
return Path(os.environ.get("ATTESTED_MEMORY_HOME") or Path.home() / ".config" / "attested-memory")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _actor_path() -> Path:
|
|
32
|
+
return _home() / "actor.json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _key_path() -> Path:
|
|
36
|
+
return _home() / "key.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_actor() -> Actor:
|
|
40
|
+
path = _actor_path()
|
|
41
|
+
if not path.exists():
|
|
42
|
+
raise SystemExit(f"no actor key at {path} — run: attested-memory init")
|
|
43
|
+
return Actor.load(path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _write_private(path: Path, payload: dict[str, Any]) -> None:
|
|
47
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
49
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
50
|
+
json.dump(payload, stream, indent=2)
|
|
51
|
+
stream.write("\n")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _client(args: argparse.Namespace, *, need_key: bool = True) -> Client:
|
|
55
|
+
api_key = os.environ.get("ATTESTED_MEMORY_API_KEY")
|
|
56
|
+
if not api_key and _key_path().exists():
|
|
57
|
+
api_key = json.loads(_key_path().read_text(encoding="utf-8")).get("api_key")
|
|
58
|
+
if need_key and not api_key:
|
|
59
|
+
raise SystemExit("no API key — run: attested-memory trial (or set ATTESTED_MEMORY_API_KEY)")
|
|
60
|
+
return Client(_load_actor(), api_key, base_url=args.url)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _print(value: Any) -> None:
|
|
64
|
+
print(json.dumps(value, indent=2, ensure_ascii=False))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
68
|
+
path = _actor_path()
|
|
69
|
+
if path.exists():
|
|
70
|
+
print(f"actor already exists: {Actor.load(path).actor_id} ({path})")
|
|
71
|
+
return 0
|
|
72
|
+
actor = Actor.generate()
|
|
73
|
+
actor.save(path)
|
|
74
|
+
print(f"created {actor.actor_id}\nkey file: {path} (mode 0600 — keep it; it is your identity)")
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cmd_whoami(args: argparse.Namespace) -> int:
|
|
79
|
+
actor = _load_actor()
|
|
80
|
+
print(actor.actor_id)
|
|
81
|
+
if _key_path().exists():
|
|
82
|
+
stored = json.loads(_key_path().read_text(encoding="utf-8"))
|
|
83
|
+
print(f"key: {stored.get('product')} {stored.get('plan')} until {stored.get('expires_at')}")
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cmd_trial(args: argparse.Namespace) -> int:
|
|
88
|
+
client = _client(args, need_key=False)
|
|
89
|
+
result = client.start_trial(args.product)
|
|
90
|
+
_write_private(_key_path(), {k: result.get(k) for k in ("api_key", "product", "plan", "expires_at", "key_id")})
|
|
91
|
+
print(f"{result.get('plan')} until {result.get('expires_at')} — key saved to {_key_path()}")
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_write(args: argparse.Namespace) -> int:
|
|
96
|
+
content = Path(args.file).read_text(encoding="utf-8") if args.file else args.content
|
|
97
|
+
if not content:
|
|
98
|
+
raise SystemExit("give the text as an argument or with --file")
|
|
99
|
+
_print(_client(args).write(args.title, content, tags=args.tag or (), visibility=args.visibility,
|
|
100
|
+
source_refs=args.source or ()))
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_read(args: argparse.Namespace) -> int:
|
|
105
|
+
_print(_client(args).read(args.memory_id))
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cmd_search(args: argparse.Namespace) -> int:
|
|
110
|
+
_print(_client(args).search(args.query, args.limit))
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
115
|
+
parser = argparse.ArgumentParser(prog="attested-memory", description="Verifiable memory for AI agents.")
|
|
116
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
117
|
+
parser.add_argument("--url", default=os.environ.get("ATTESTED_MEMORY_URL", DEFAULT_BASE_URL))
|
|
118
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
119
|
+
sub.add_parser("init", help="create your actor key").set_defaults(func=cmd_init)
|
|
120
|
+
sub.add_parser("whoami", help="show your actor id and key").set_defaults(func=cmd_whoami)
|
|
121
|
+
trial = sub.add_parser("trial", help="claim the free trial key for your actor")
|
|
122
|
+
trial.add_argument("--product", default="personal", choices=["personal", "team", "expert-market"])
|
|
123
|
+
trial.set_defaults(func=cmd_trial)
|
|
124
|
+
write = sub.add_parser("write", help="store a memory")
|
|
125
|
+
write.add_argument("title")
|
|
126
|
+
write.add_argument("content", nargs="?")
|
|
127
|
+
write.add_argument("--file")
|
|
128
|
+
write.add_argument("--tag", action="append")
|
|
129
|
+
write.add_argument("--source", action="append", help="a source reference (URL, doc id)")
|
|
130
|
+
write.add_argument("--visibility", default="private", choices=["private", "shared", "public", "paid"])
|
|
131
|
+
write.set_defaults(func=cmd_write)
|
|
132
|
+
read = sub.add_parser("read", help="read a memory (content checked against its hash)")
|
|
133
|
+
read.add_argument("memory_id")
|
|
134
|
+
read.set_defaults(func=cmd_read)
|
|
135
|
+
search = sub.add_parser("search", help="search memories you can see")
|
|
136
|
+
search.add_argument("query", nargs="?", default="")
|
|
137
|
+
search.add_argument("--limit", type=int, default=30)
|
|
138
|
+
search.set_defaults(func=cmd_search)
|
|
139
|
+
return parser
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main(argv: list[str] | None = None) -> int:
|
|
143
|
+
args = build_parser().parse_args(argv)
|
|
144
|
+
try:
|
|
145
|
+
return args.func(args)
|
|
146
|
+
except AttestedMemoryError as error:
|
|
147
|
+
print(f"attested-memory: {error}", file=sys.stderr)
|
|
148
|
+
return 1
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""HTTP client for the public Attested Memory API (Personal Memory product).
|
|
2
|
+
|
|
3
|
+
actor = Actor.generate()
|
|
4
|
+
client = Client(actor)
|
|
5
|
+
client.start_trial() # 7-day personal key, bound to this actor
|
|
6
|
+
unit = client.write("Supplier terms", "Net 30, EUR, signed 2026-09-01", tags=["finance"])
|
|
7
|
+
client.read(unit["id"]) # content checked against its content_hash
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.parse
|
|
16
|
+
import urllib.request
|
|
17
|
+
from typing import Any, Iterable
|
|
18
|
+
|
|
19
|
+
from . import __version__
|
|
20
|
+
from .actor import Actor
|
|
21
|
+
|
|
22
|
+
DEFAULT_BASE_URL = "https://attestedmemory.net"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AttestedMemoryError(Exception):
|
|
26
|
+
"""The service refused or failed a request; ``status`` is the HTTP code (0 = network)."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, status: int, detail: str) -> None:
|
|
29
|
+
super().__init__(f"HTTP {status}: {detail}" if status else detail)
|
|
30
|
+
self.status = status
|
|
31
|
+
self.detail = detail
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class IntegrityError(AttestedMemoryError):
|
|
35
|
+
"""A memory's content does not hash to the content_hash the service reported."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def content_hash(content: str) -> str:
|
|
39
|
+
"""The Memory Market's hash of a memory's text: ``sha256:`` + hex."""
|
|
40
|
+
return "sha256:" + hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_integrity(unit: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
+
"""Raise IntegrityError unless ``unit['content']`` hashes to ``unit['content_hash']``.
|
|
45
|
+
|
|
46
|
+
This is checked here, on the client, not taken from the service. The truth and
|
|
47
|
+
provenance states in the same object are what the hub reports.
|
|
48
|
+
"""
|
|
49
|
+
if "content" in unit and unit.get("content_hash") != content_hash(unit["content"]):
|
|
50
|
+
raise IntegrityError(0, f"memory {unit.get('id')}: content does not match its content_hash")
|
|
51
|
+
return unit
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Client:
|
|
55
|
+
def __init__(self, actor: Actor, api_key: str | None = None, *,
|
|
56
|
+
base_url: str = DEFAULT_BASE_URL, timeout: float = 20.0) -> None:
|
|
57
|
+
self.actor = actor
|
|
58
|
+
self.api_key = api_key
|
|
59
|
+
self.base_url = base_url.rstrip("/")
|
|
60
|
+
self.timeout = timeout
|
|
61
|
+
|
|
62
|
+
# --- account -------------------------------------------------------------------------
|
|
63
|
+
def start_trial(self, product: str = "personal") -> dict[str, Any]:
|
|
64
|
+
"""Claim the free trial key for this actor (one per actor and product) and keep it."""
|
|
65
|
+
result = self._request("POST", "/api/v1/trials", {"product": product}, need_key=False)
|
|
66
|
+
self.api_key = result.get("api_key") or self.api_key
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def key_status(self, product: str = "personal") -> dict[str, Any]:
|
|
70
|
+
return self._request("GET", "/api/v1/keys/me?" + urllib.parse.urlencode({"product": product}))
|
|
71
|
+
|
|
72
|
+
# --- memory --------------------------------------------------------------------------
|
|
73
|
+
def write(self, title: str, content: str, *, tags: Iterable[str] = (), visibility: str = "private",
|
|
74
|
+
source_refs: Iterable[str] = (), parent_memory_ids: Iterable[str] = (),
|
|
75
|
+
price_usdc: str | None = None) -> dict[str, Any]:
|
|
76
|
+
body = {"title": title, "content": content, "tags": list(tags), "visibility": visibility,
|
|
77
|
+
"source_refs": list(source_refs), "parent_memory_ids": list(parent_memory_ids),
|
|
78
|
+
"price_usdc": price_usdc}
|
|
79
|
+
return check_integrity(self._request("POST", "/memory/api/memories", body))
|
|
80
|
+
|
|
81
|
+
def read(self, memory_id: str) -> dict[str, Any]:
|
|
82
|
+
path = "/memory/api/memories/" + urllib.parse.quote(memory_id, safe="")
|
|
83
|
+
return check_integrity(self._request("GET", path))
|
|
84
|
+
|
|
85
|
+
def search(self, query: str = "", limit: int = 30) -> dict[str, Any]:
|
|
86
|
+
params = urllib.parse.urlencode({"q": query, "limit": max(1, min(int(limit), 100))})
|
|
87
|
+
return self._request("GET", "/memory/api/search?" + params)
|
|
88
|
+
|
|
89
|
+
# --- transport -----------------------------------------------------------------------
|
|
90
|
+
def _request(self, method: str, path: str, body: dict[str, Any] | None = None, *,
|
|
91
|
+
need_key: bool = True) -> Any:
|
|
92
|
+
headers = {"Accept": "application/json", "User-Agent": f"attested-memory-python/{__version__}",
|
|
93
|
+
**self.actor.proof_headers()}
|
|
94
|
+
if need_key:
|
|
95
|
+
if not self.api_key:
|
|
96
|
+
raise AttestedMemoryError(0, "no API key: call start_trial() or pass api_key")
|
|
97
|
+
headers["X-SaaS-Key"] = self.api_key
|
|
98
|
+
data = None
|
|
99
|
+
if body is not None:
|
|
100
|
+
data = json.dumps(body).encode("utf-8")
|
|
101
|
+
headers["Content-Type"] = "application/json"
|
|
102
|
+
request = urllib.request.Request(self.base_url + path, data=data, method=method, headers=headers)
|
|
103
|
+
try:
|
|
104
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
105
|
+
raw = response.read()
|
|
106
|
+
except urllib.error.HTTPError as error:
|
|
107
|
+
raise AttestedMemoryError(error.code, _detail(error.read())) from None
|
|
108
|
+
except (urllib.error.URLError, TimeoutError, OSError) as error:
|
|
109
|
+
raise AttestedMemoryError(0, f"cannot reach {self.base_url}: {error}") from None
|
|
110
|
+
return json.loads(raw) if raw else {}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _detail(raw: bytes) -> str:
|
|
114
|
+
try:
|
|
115
|
+
payload = json.loads(raw)
|
|
116
|
+
except ValueError:
|
|
117
|
+
return raw.decode("utf-8", "replace")[:300]
|
|
118
|
+
detail = payload.get("detail") if isinstance(payload, dict) else None
|
|
119
|
+
return str(detail if detail is not None else payload)[:300]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: attested-memory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Client and CLI for Attested Memory: write, search and read verifiable AI-agent memory under a signed Ed25519 actor identity.
|
|
5
|
+
Project-URL: Homepage, https://attestedmemory.net/
|
|
6
|
+
Project-URL: Source, https://github.com/alexar76/attested-memory
|
|
7
|
+
Author: AIMarket
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent-memory,ai-agents,ed25519,memory-poisoning,provenance,verifiable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Security :: Cryptography
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Requires-Dist: cryptography>=41
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# attested-memory
|
|
22
|
+
|
|
23
|
+
Python client and CLI for [Attested Memory](https://attestedmemory.net/) — verifiable memory
|
|
24
|
+
for AI agents. Every call is signed by your own Ed25519 **actor** key; the private key never
|
|
25
|
+
leaves your machine. A memory you read is checked here, on the client, against the
|
|
26
|
+
`content_hash` the hub recorded, so a changed text is refused instead of trusted.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install attested-memory
|
|
30
|
+
attested-memory init # your actor key → ~/.config/attested-memory/actor.json (0600)
|
|
31
|
+
attested-memory trial # 7-day personal key, bound to that actor
|
|
32
|
+
attested-memory write "Supplier terms" "Net 30, EUR, signed 2026-09-01" --tag finance --source contract-17
|
|
33
|
+
attested-memory search supplier
|
|
34
|
+
attested-memory read mem_…
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from attested_memory import Actor, Client
|
|
39
|
+
|
|
40
|
+
actor = Actor.generate() # or Actor.load("actor.json")
|
|
41
|
+
client = Client(actor)
|
|
42
|
+
client.start_trial() # or Client(actor, api_key="ask_…")
|
|
43
|
+
unit = client.write("Supplier terms", "Net 30", tags=["finance"])
|
|
44
|
+
client.read(unit["id"]) # raises IntegrityError if the text does not match its hash
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
What is checked where:
|
|
48
|
+
|
|
49
|
+
| | where |
|
|
50
|
+
|---|---|
|
|
51
|
+
| your identity (`did:actor:` + SHA-256 of your public key) and a one-time signed proof per request | client signs, server verifies |
|
|
52
|
+
| the text of a memory matches its `content_hash` | **this client** |
|
|
53
|
+
| `truth` (supported / contested / rejected) and `provenance` (attested receipt) | reported by the hub |
|
|
54
|
+
|
|
55
|
+
The proof is a signature over `<actor id>\n<unix seconds>\n<nonce>`: good for five minutes and
|
|
56
|
+
one request, so a captured header set replays nowhere.
|
|
57
|
+
|
|
58
|
+
Environment for the CLI: `ATTESTED_MEMORY_HOME`, `ATTESTED_MEMORY_URL`, `ATTESTED_MEMORY_API_KEY`.
|
|
59
|
+
|
|
60
|
+
Apache-2.0. Source: [alexar76/attested-memory](https://github.com/alexar76/attested-memory) (`clients/python`).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
attested_memory/__init__.py,sha256=lyzStnftEkJJ84sRPPEe53cl2C6izfyiALK3mZuswds,382
|
|
2
|
+
attested_memory/actor.py,sha256=hkiQkzdHOrJV0_IIa-ZaklGcw9NTqHlQ8UzxfxxR3Ns,3708
|
|
3
|
+
attested_memory/cli.py,sha256=NUgSHKlG47_eaanSB7MG_2Qo1u6rpejSze5xuWrVaAA,5787
|
|
4
|
+
attested_memory/client.py,sha256=-xEDXmdBg5qaGNl1O419D6ZleEOTsq-fVrJjEQd97uo,5453
|
|
5
|
+
attested_memory-0.1.0.dist-info/METADATA,sha256=BIXlGNDhJPooRvg-xvGXBbSriNYPa4UOnkInFXPcr7U,2698
|
|
6
|
+
attested_memory-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
attested_memory-0.1.0.dist-info/entry_points.txt,sha256=1csrTCjvea6Byg2l9bXdqkO-yv5Z3b8MSrObVgAPXkI,61
|
|
8
|
+
attested_memory-0.1.0.dist-info/licenses/LICENSE,sha256=FkIqCjBmfAAgdAOWCOggpfp-BEHHObGox599lEvM-l8,647
|
|
9
|
+
attested_memory-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Attested Memory Hub contributors
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|