cortexm 0.3.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.
Files changed (120) hide show
  1. context_m.py +17 -0
  2. cortexm/__init__.py +45 -0
  3. cortexm/accel.py +403 -0
  4. cortexm/api/__init__.py +0 -0
  5. cortexm/api/chaos.py +118 -0
  6. cortexm/api/memory.py +635 -0
  7. cortexm/bench/__init__.py +0 -0
  8. cortexm/bench/abilities.py +311 -0
  9. cortexm/bench/baselines.py +89 -0
  10. cortexm/bench/beam_loader.py +317 -0
  11. cortexm/bench/generator.py +376 -0
  12. cortexm/bench/harness.py +211 -0
  13. cortexm/bench/messy.py +218 -0
  14. cortexm/bench/micro.py +251 -0
  15. cortexm/bench/ood.py +443 -0
  16. cortexm/bench/run.py +137 -0
  17. cortexm/bridge/__init__.py +0 -0
  18. cortexm/bridge/dates.py +178 -0
  19. cortexm/bridge/decoders.py +204 -0
  20. cortexm/bridge/enrich.py +255 -0
  21. cortexm/bridge/extractor.py +316 -0
  22. cortexm/bridge/fallback.py +332 -0
  23. cortexm/bridge/onnx_runtime.py +158 -0
  24. cortexm/bridge/patterns.py +760 -0
  25. cortexm/bridge/ppr.py +104 -0
  26. cortexm/bridge/prefilter.py +188 -0
  27. cortexm/bridge/query_extract.py +420 -0
  28. cortexm/bridge/reader.py +1174 -0
  29. cortexm/bridge/rerank.py +204 -0
  30. cortexm/bridge/writer.py +492 -0
  31. cortexm/cli.py +295 -0
  32. cortexm/cognition/__init__.py +53 -0
  33. cortexm/cognition/abstraction.py +192 -0
  34. cortexm/cognition/analogy.py +159 -0
  35. cortexm/cognition/engine.py +204 -0
  36. cortexm/cognition/gaps.py +365 -0
  37. cortexm/cognition/scanner.py +204 -0
  38. cortexm/config.py +375 -0
  39. cortexm/cortexm.py +8 -0
  40. cortexm/enterprise/__init__.py +0 -0
  41. cortexm/enterprise/audit.py +178 -0
  42. cortexm/enterprise/governance.py +239 -0
  43. cortexm/errors.py +35 -0
  44. cortexm/features/__init__.py +0 -0
  45. cortexm/features/git.py +204 -0
  46. cortexm/features/prefetch.py +88 -0
  47. cortexm/features/zk.py +105 -0
  48. cortexm/federation/__init__.py +39 -0
  49. cortexm/federation/crdt.py +275 -0
  50. cortexm/federation/fabric.py +109 -0
  51. cortexm/federation/hlc.py +80 -0
  52. cortexm/federation/node.py +145 -0
  53. cortexm/federation/schema_report.py +73 -0
  54. cortexm/federation/transport.py +164 -0
  55. cortexm/index/__init__.py +19 -0
  56. cortexm/index/nsg.py +386 -0
  57. cortexm/mcp/__init__.py +0 -0
  58. cortexm/mcp/server.py +985 -0
  59. cortexm/metrics.py +62 -0
  60. cortexm/migrate/__init__.py +0 -0
  61. cortexm/migrate/importers.py +192 -0
  62. cortexm/provenance/__init__.py +78 -0
  63. cortexm/provenance/agent.py +214 -0
  64. cortexm/provenance/cose.py +201 -0
  65. cortexm/provenance/scitt.py +258 -0
  66. cortexm/provenance/vc.py +250 -0
  67. cortexm/security/__init__.py +0 -0
  68. cortexm/security/crypto.py +162 -0
  69. cortexm/security/hashes.py +140 -0
  70. cortexm/security/injection.py +149 -0
  71. cortexm/security/mind.py +154 -0
  72. cortexm/security/pii.py +265 -0
  73. cortexm/security/rbac.py +169 -0
  74. cortexm/security/sandbox.py +131 -0
  75. cortexm/security/zk_hamming.py +142 -0
  76. cortexm/security/zk_sql.py +485 -0
  77. cortexm/server/__init__.py +0 -0
  78. cortexm/server/metrics.py +88 -0
  79. cortexm/server/rest.py +936 -0
  80. cortexm/server/sparql.py +984 -0
  81. cortexm/text/__init__.py +0 -0
  82. cortexm/text/dissim.py +252 -0
  83. cortexm/text/embedder.py +155 -0
  84. cortexm/text/fuzzy.py +218 -0
  85. cortexm/text/idiolect.py +253 -0
  86. cortexm/text/labse.py +374 -0
  87. cortexm/text/tokenizer.py +79 -0
  88. cortexm/trace/__init__.py +0 -0
  89. cortexm/trace/blob_arena.py +277 -0
  90. cortexm/trace/consolidate.py +337 -0
  91. cortexm/trace/contradictions.py +69 -0
  92. cortexm/trace/dedup.py +114 -0
  93. cortexm/trace/edges.py +214 -0
  94. cortexm/trace/fact.py +121 -0
  95. cortexm/trace/fade.py +245 -0
  96. cortexm/trace/lifecycle.py +112 -0
  97. cortexm/trace/rebuild.py +173 -0
  98. cortexm/trace/rules.py +171 -0
  99. cortexm/trace/store.py +680 -0
  100. cortexm/trace/structural.py +183 -0
  101. cortexm/trace/tmt.py +335 -0
  102. cortexm/util.py +148 -0
  103. cortexm/vsa/__init__.py +0 -0
  104. cortexm/vsa/attribution.py +149 -0
  105. cortexm/vsa/cleanup.py +161 -0
  106. cortexm/vsa/codecs.py +397 -0
  107. cortexm/vsa/hologram_overlay.py +139 -0
  108. cortexm/vsa/index.py +163 -0
  109. cortexm/vsa/ops.py +149 -0
  110. cortexm/vsa/palace.py +446 -0
  111. cortexm/vsa/role_vectors.py +236 -0
  112. cortexm/vsa/slb.py +78 -0
  113. cortexm/vsa/tlsh_trie.py +137 -0
  114. cortexm/vsa/working_memory.py +249 -0
  115. cortexm-0.3.0.dist-info/METADATA +482 -0
  116. cortexm-0.3.0.dist-info/RECORD +120 -0
  117. cortexm-0.3.0.dist-info/WHEEL +5 -0
  118. cortexm-0.3.0.dist-info/entry_points.txt +2 -0
  119. cortexm-0.3.0.dist-info/licenses/LICENSE +190 -0
  120. cortexm-0.3.0.dist-info/top_level.txt +2 -0
cortexm/metrics.py ADDED
@@ -0,0 +1,62 @@
1
+ """Process-wide counters backing the μ=0 (zero-LLM-ingest) protocol.
2
+
3
+ Every code path that could invoke a language model must route through
4
+ ``call_llm``. The default implementation raises: the deterministic core
5
+ physically cannot make an LLM call. Optional async enrichment (never used
6
+ during benchmarks) increments the counter so audits can prove honesty.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+
13
+ _LOCK = threading.Lock()
14
+ _LLM_CALLS = 0
15
+ _INGESTED_TOKENS = 0
16
+ _INGESTED_MESSAGES = 0
17
+ _EXTRACTED_FACTS = 0
18
+ _RETRIEVALS = 0
19
+
20
+
21
+ def llm_calls() -> int:
22
+ return _LLM_CALLS
23
+
24
+
25
+ def bump_llm_call() -> None:
26
+ global _LLM_CALLS
27
+ with _LOCK:
28
+ _LLM_CALLS += 1
29
+
30
+
31
+ def bump_ingest(tokens: int = 0, messages: int = 0, facts: int = 0) -> None:
32
+ global _INGESTED_TOKENS, _INGESTED_MESSAGES, _EXTRACTED_FACTS
33
+ with _LOCK:
34
+ _INGESTED_TOKENS += tokens
35
+ _INGESTED_MESSAGES += messages
36
+ _EXTRACTED_FACTS += facts
37
+
38
+
39
+ def bump_retrieval() -> None:
40
+ global _RETRIEVALS
41
+ with _LOCK:
42
+ _RETRIEVALS += 1
43
+
44
+
45
+ def counters() -> dict:
46
+ return {
47
+ "llm_calls": _LLM_CALLS, # must be 0 under μ=0 protocol
48
+ "ingested_tokens": _INGESTED_TOKENS,
49
+ "ingested_messages": _INGESTED_MESSAGES,
50
+ "extracted_facts": _EXTRACTED_FACTS,
51
+ "retrievals": _RETRIEVALS,
52
+ }
53
+
54
+
55
+ def reset_counters() -> None:
56
+ global _LLM_CALLS, _INGESTED_TOKENS, _INGESTED_MESSAGES, _EXTRACTED_FACTS, _RETRIEVALS
57
+ with _LOCK:
58
+ _LLM_CALLS = 0
59
+ _INGESTED_TOKENS = 0
60
+ _INGESTED_MESSAGES = 0
61
+ _EXTRACTED_FACTS = 0
62
+ _RETRIEVALS = 0
File without changes
@@ -0,0 +1,192 @@
1
+ """Migration importers — Mem0 / Zep / Chroma → Context-M.
2
+
3
+ The plan's Trojan-Horse migration tooling: read a competitor's local
4
+ store, convert to Trace triples (μ=0 re-extraction on raw text where
5
+ possible — timestamps preserved), and keep the original ids in
6
+ provenance for audit.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import sqlite3
14
+ from datetime import datetime, timezone
15
+
16
+ from cortexm.errors import MigrationError
17
+ from cortexm.util import parse_ts
18
+
19
+
20
+ def _iter_mem0_rows(path: str):
21
+ """Yield (text, created_at) from a Mem0 SQLite store.
22
+
23
+ Mem0's local store keeps a ``history`` table with JSON payloads
24
+ containing the original conversation plus extracted memories.
25
+ """
26
+ if not os.path.exists(path):
27
+ raise MigrationError(f"mem0 store not found: {path}")
28
+ conn = sqlite3.connect(path)
29
+ conn.row_factory = sqlite3.Row
30
+ try:
31
+ tables = {r[0] for r in conn.execute(
32
+ "SELECT name FROM sqlite_master WHERE type='table'")}
33
+ if "history" in tables:
34
+ for row in conn.execute(
35
+ "SELECT data, created_at, updated_at FROM history "
36
+ "ORDER BY id"):
37
+ raw = row["data"] or ""
38
+ created = row["created_at"] or row["updated_at"]
39
+ try:
40
+ payload = json.loads(raw)
41
+ except (json.JSONDecodeError, TypeError):
42
+ payload = None
43
+ texts: list[str] = []
44
+ if isinstance(payload, dict):
45
+ for msg in payload.get("messages", []):
46
+ if isinstance(msg, dict) and msg.get("content"):
47
+ texts.append(str(msg["content"]))
48
+ for mem in payload.get("memories", []):
49
+ if isinstance(mem, str):
50
+ texts.append(mem)
51
+ elif isinstance(raw, str) and raw.strip():
52
+ texts.append(raw.strip())
53
+ for t in texts:
54
+ if t and len(t) > 2:
55
+ yield t, created
56
+ elif "memories" in tables:
57
+ cols = {r[1] for r in conn.execute("PRAGMA table_info(memories)")}
58
+ text_col = "data" if "data" in cols else (
59
+ "memory" if "memory" in cols else "text")
60
+ for row in conn.execute(f"SELECT {text_col} AS t, created_at "
61
+ f"FROM memories ORDER BY rowid"):
62
+ if row["t"]:
63
+ yield str(row["t"]), row["created_at"]
64
+ else:
65
+ raise MigrationError(
66
+ f"unrecognized mem0 schema (tables: {sorted(tables)})")
67
+ finally:
68
+ conn.close()
69
+
70
+
71
+ def _iter_zep_rows(path: str):
72
+ """Yield from a Zep JSONL export: {"subject","relation","object",
73
+ "valid_at","invalid_at"} or {"text","created_at"} rows."""
74
+ if not os.path.exists(path):
75
+ raise MigrationError(f"zep export not found: {path}")
76
+ with open(path, encoding="utf-8") as fh:
77
+ for line in fh:
78
+ line = line.strip()
79
+ if not line:
80
+ continue
81
+ try:
82
+ row = json.loads(line)
83
+ except json.JSONDecodeError:
84
+ continue
85
+ yield row
86
+
87
+
88
+ def _iter_chroma_rows(path: str):
89
+ """Yield documents from a chroma.sqlite3 store."""
90
+ if not os.path.exists(path):
91
+ raise MigrationError(f"chroma store not found: {path}")
92
+ conn = sqlite3.connect(path)
93
+ conn.row_factory = sqlite3.Row
94
+ try:
95
+ tables = {r[0] for r in conn.execute(
96
+ "SELECT name FROM sqlite_master WHERE type='table'")}
97
+ if "embeddings" in tables:
98
+ cols = {r[1] for r in conn.execute(
99
+ "PRAGMA table_info(embeddings)")}
100
+ doc_col = None
101
+ for c in ("document", "documents", "content", "data"):
102
+ if c in cols:
103
+ doc_col = c
104
+ break
105
+ if doc_col:
106
+ for row in conn.execute(
107
+ f"SELECT {doc_col} AS doc FROM embeddings"):
108
+ if row["doc"]:
109
+ yield str(row["doc"]), None
110
+ return
111
+ if "documents" in tables:
112
+ for row in conn.execute("SELECT * FROM documents"):
113
+ for v in row:
114
+ if v:
115
+ yield str(v), None
116
+ return
117
+ raise MigrationError(f"unrecognized chroma schema: {sorted(tables)}")
118
+ finally:
119
+ conn.close()
120
+
121
+
122
+ # ------------------------------------------------------------------
123
+ def import_mem0(memory, path: str, user_id: str = "migrated",
124
+ batch: int = 200) -> dict:
125
+ """Import a Mem0 store into Context-M (μ=0 re-extraction)."""
126
+ n_msgs, n_facts = 0, 0
127
+ buf, last_ts = [], None
128
+ for text, created in _iter_mem0_rows(path):
129
+ buf.append(text)
130
+ last_ts = created
131
+ if len(buf) >= batch:
132
+ out = memory.add(buf, user_id=user_id,
133
+ timestamp=parse_ts(last_ts) or
134
+ datetime.now(timezone.utc))
135
+ n_msgs += len(buf)
136
+ n_facts += out["stats"]["facts_inserted"]
137
+ buf = []
138
+ if buf:
139
+ out = memory.add(buf, user_id=user_id,
140
+ timestamp=parse_ts(last_ts) or
141
+ datetime.now(timezone.utc))
142
+ n_msgs += len(buf)
143
+ n_facts += out["stats"]["facts_inserted"]
144
+ return {"source": "mem0", "messages": n_msgs, "facts": n_facts}
145
+
146
+
147
+ def import_zep(memory, path: str, user_id: str = "migrated") -> dict:
148
+ """Import a Zep JSONL export. Graph triples keep bi-temporal windows;
149
+ raw text rows are re-extracted (μ=0)."""
150
+ from cortexm.trace.fact import make_fact
151
+ n_triples, n_texts = 0, 0
152
+ commit = memory.store.create_commit("migrate: zep")
153
+ for row in _iter_zep_rows(path):
154
+ if all(k in row for k in ("subject", "relation", "object")):
155
+ f = make_fact(row["subject"], row["relation"], row["object"],
156
+ now=parse_ts(row.get("valid_at")) or
157
+ datetime.now(timezone.utc),
158
+ valid_to=row.get("invalid_at"),
159
+ user_id=user_id, confidence=0.8,
160
+ provenance={"migrated_from": "zep"})
161
+ memory.store.insert_fact(f, commit)
162
+ memory.palace.add(f.id, memory.palace.encode_fact(f))
163
+ n_triples += 1
164
+ elif row.get("text"):
165
+ memory.add(row["text"], user_id=user_id)
166
+ n_texts += 1
167
+ memory.store.end_batch()
168
+ memory.reader.invalidate_caches()
169
+ return {"source": "zep", "triples": n_triples, "texts": n_texts}
170
+
171
+
172
+ def import_chroma(memory, path: str, user_id: str = "migrated",
173
+ batch: int = 200) -> dict:
174
+ """Import documents from a Chroma store (μ=0 re-extraction; foreign
175
+ embeddings are NOT portable across embedding models)."""
176
+ n_msgs, n_facts = 0, 0
177
+ buf = []
178
+ for doc, _ts in _iter_chroma_rows(path):
179
+ buf.append(doc)
180
+ if len(buf) >= batch:
181
+ out = memory.add(buf, user_id=user_id)
182
+ n_msgs += len(buf)
183
+ n_facts += out["stats"]["facts_inserted"]
184
+ buf = []
185
+ if buf:
186
+ out = memory.add(buf, user_id=user_id)
187
+ n_msgs += len(buf)
188
+ n_facts += out["stats"]["facts_inserted"]
189
+ return {"source": "chroma", "documents": n_msgs, "facts": n_facts}
190
+
191
+
192
+ MIGRATORS = {"mem0": import_mem0, "zep": import_zep, "chroma": import_chroma}
@@ -0,0 +1,78 @@
1
+ """Provenance Standards Stack — enterprise-grade provenance wrappers.
2
+
3
+ Context-M's internal integrity is BLAKE3 source hashing + a hash-chained
4
+ audit log. This is cryptographically sound but NOT standards-compliant.
5
+ Enterprise security reviews ask: "Does it support W3C VC? C2PA? SCITT?"
6
+ — this layer answers "yes" by wrapping the existing BLAKE3 hashes into
7
+ standards-compliant envelopes.
8
+
9
+ Three sub-modules:
10
+
11
+ cose.py — COSE Sign1 envelopes (RFC 9052) using Ed25519. Every
12
+ memory commit gets wrapped in a COSE Sign1 with the
13
+ agent's Ed25519 key, so external verifiers can confirm
14
+ the commit came from a known agent without trusting the
15
+ host's audit log.
16
+
17
+ vc.py — W3C Verifiable Credentials 2.0 export. A memory range
18
+ (e.g. "all facts about user=alice from 2026-01-01 to
19
+ 2026-02-01") can be exported as a W3C VC Data Integrity
20
+ proof (eddsa-jcs-2022). The VC contains the BLAKE3 root
21
+ of the range + the COSE Sign1 envelope that proves the
22
+ agent signed it.
23
+
24
+ scitt.py — SCITT-signed statements. Forwards a COSE Sign1 to a
25
+ (mocked) SCITT transparency log, returns a receipt that
26
+ can be verified by a third party.
27
+
28
+ Design: BLAKE3 source hashing stays as the internal integrity mechanism.
29
+ This layer SITS ON TOP for external interoperability. No replacement —
30
+ just a wrapper. Existing `verify_integrity()` stays; the standards
31
+ layer adds `verify_with_standards()` that calls both.
32
+
33
+ Pure Python — no halo2, no BBS+, no JWT libs. Ed25519 uses the
34
+ Python stdlib `hashlib` for hashing and a tiny ed25519 implementation
35
+ fallback if `cryptography` is not available. The COSE/VC/SCITT wire
36
+ formats are JSON (CBOR is preferred per RFC 9052, but JSON works for
37
+ the prototype).
38
+ """
39
+
40
+ from cortexm.provenance.cose import (
41
+ CoseSign1Envelope,
42
+ sign_commit,
43
+ verify_commit,
44
+ )
45
+ from cortexm.provenance.vc import (
46
+ VerifiableCredential,
47
+ export_memory_range_vc,
48
+ verify_vc,
49
+ )
50
+ from cortexm.provenance.scitt import (
51
+ ScittStatement,
52
+ ScittReceipt,
53
+ submit_to_scitt,
54
+ verify_receipt,
55
+ reset_scitt_log,
56
+ get_scitt_service_did,
57
+ )
58
+ from cortexm.provenance.agent import (
59
+ Ed25519AgentKey,
60
+ get_default_agent,
61
+ set_default_agent,
62
+ )
63
+
64
+ __all__ = [
65
+ # cose
66
+ "CoseSign1Envelope", "Ed25519AgentKey",
67
+ "sign_commit", "verify_commit",
68
+ # vc
69
+ "VerifiableCredential",
70
+ "export_memory_range_vc", "verify_vc",
71
+ # scitt
72
+ "ScittStatement", "ScittReceipt",
73
+ "submit_to_scitt", "verify_receipt",
74
+ "reset_scitt_log", "get_scitt_service_did",
75
+ # agent
76
+ "Ed25519AgentKey",
77
+ "get_default_agent", "set_default_agent",
78
+ ]
@@ -0,0 +1,214 @@
1
+ """ProvenanceAgent — Ed25519 identity for memory commits.
2
+
3
+ Each agent (LLM instance, microservice, scheduled job) that writes to
4
+ the Trace has an Ed25519 signing key. The agent's public key is
5
+ exposed as a `did:key` identifier so external verifiers can resolve
6
+ the key from the DID without an external registry.
7
+
8
+ The agent key is persisted to a tiny PEM-ish file (path from
9
+ Config.provenance_agent_key_path). If no path is set, the agent uses
10
+ a process-local ephemeral key (regenerated on restart — fine for
11
+ tests, not for production cross-system verification).
12
+
13
+ Uses the `cryptography` library for Ed25519 (constant-time, audited).
14
+ If `cryptography` is unavailable, falls back to an HMAC-SHA256 pseudo-
15
+ signature clearly labeled "DEV-MODE" — NOT for production.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import base64
21
+ import hashlib
22
+ import hmac
23
+ import os
24
+ from dataclasses import dataclass
25
+
26
+
27
+ @dataclass
28
+ class Ed25519AgentKey:
29
+ """An Ed25519 signing keypair for a provenance agent."""
30
+ private_key: bytes # 32-byte seed (Ed25519 form)
31
+ public_key: bytes # 32-byte pubkey
32
+ did: str = "" # did:key identifier — derived from pubkey
33
+ label: str = "context-m-agent"
34
+ dev_mode: bool = False # True when using HMAC fallback (no cryptography lib)
35
+
36
+ @classmethod
37
+ def generate(cls, label: str = "context-m-agent") -> "Ed25519AgentKey":
38
+ """Generate a fresh Ed25519 keypair."""
39
+ try:
40
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
41
+ Ed25519PrivateKey)
42
+ from cryptography.hazmat.primitives.serialization import (
43
+ Encoding, PrivateFormat, NoEncryption, PublicFormat)
44
+ priv = Ed25519PrivateKey.generate()
45
+ priv_bytes = priv.private_bytes(
46
+ encoding=Encoding.Raw,
47
+ format=PrivateFormat.Raw,
48
+ encryption_algorithm=NoEncryption(),
49
+ )
50
+ pub_bytes = priv.public_key().public_bytes(
51
+ encoding=Encoding.Raw,
52
+ format=PublicFormat.Raw,
53
+ )
54
+ did = _did_key_from_pubkey(pub_bytes)
55
+ return cls(
56
+ private_key=priv_bytes,
57
+ public_key=pub_bytes,
58
+ did=did,
59
+ label=label,
60
+ )
61
+ except ImportError:
62
+ # DEV fallback: HMAC-SHA256 — NOT for production.
63
+ seed = os.urandom(32)
64
+ return cls(
65
+ private_key=seed,
66
+ public_key=seed, # symmetric — fallback only
67
+ did=f"did:dev:{hashlib.sha256(seed).hexdigest()[:32]}",
68
+ label=label,
69
+ dev_mode=True,
70
+ )
71
+
72
+ @classmethod
73
+ def from_pem(cls, pem_path: str,
74
+ label: str = "context-m-agent") -> "Ed25519AgentKey":
75
+ """Load a keypair from a PEM-ish file. Generates one if missing."""
76
+ if not os.path.exists(pem_path):
77
+ key = cls.generate(label=label)
78
+ key.save_pem(pem_path)
79
+ return key
80
+ try:
81
+ with open(pem_path, "rb") as f:
82
+ text = f.read().decode("utf-8")
83
+ lines = [l.strip() for l in text.splitlines()
84
+ if l and not l.startswith("-")]
85
+ priv = base64.b64decode(lines[0])
86
+ pub = base64.b64decode(lines[1]) if len(lines) > 1 else b""
87
+ if len(priv) != 32:
88
+ raise ValueError("private key must be 32 bytes")
89
+ if not pub or len(pub) != 32:
90
+ # regenerate pub from priv
91
+ try:
92
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
93
+ Ed25519PrivateKey)
94
+ from cryptography.hazmat.primitives.serialization import (
95
+ Encoding, PublicFormat)
96
+ pub = (Ed25519PrivateKey.from_private_bytes(priv)
97
+ .public_key().public_bytes(
98
+ encoding=Encoding.Raw,
99
+ format=PublicFormat.Raw))
100
+ except ImportError:
101
+ pub = priv
102
+ did = _did_key_from_pubkey(pub) if len(pub) == 32 else f"did:dev:{hashlib.sha256(priv).hexdigest()[:32]}"
103
+ return cls(
104
+ private_key=priv, public_key=pub, did=did, label=label,
105
+ dev_mode=(len(pub) != 32))
106
+ except Exception:
107
+ key = cls.generate(label=label)
108
+ key.save_pem(pem_path)
109
+ return key
110
+
111
+ def save_pem(self, pem_path: str) -> None:
112
+ """Persist the keypair to a tiny PEM-ish file."""
113
+ os.makedirs(os.path.dirname(pem_path) or ".", exist_ok=True)
114
+ text = (
115
+ f"-----BEGIN ED25519 PRIVATE KEY-----\n"
116
+ f"{base64.b64encode(self.private_key).decode()}\n"
117
+ f"-----END ED25519 PRIVATE KEY-----\n"
118
+ f"-----BEGIN ED25519 PUBLIC KEY-----\n"
119
+ f"{base64.b64encode(self.public_key).decode()}\n"
120
+ f"-----END ED25519 PUBLIC KEY-----\n"
121
+ f"LABEL: {self.label}\n"
122
+ )
123
+ with open(pem_path, "w") as f:
124
+ f.write(text)
125
+ try:
126
+ os.chmod(pem_path, 0o600)
127
+ except Exception:
128
+ pass
129
+
130
+ def sign(self, message: bytes) -> bytes:
131
+ """Sign a message with the agent's private key."""
132
+ if self.dev_mode:
133
+ # DEV fallback — HMAC-SHA256, clearly labeled
134
+ mac = hmac.new(self.private_key, message, hashlib.sha256).digest()
135
+ return b"DEV-MODE:" + mac
136
+ try:
137
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
138
+ Ed25519PrivateKey)
139
+ priv = Ed25519PrivateKey.from_private_bytes(self.private_key)
140
+ return priv.sign(message)
141
+ except Exception:
142
+ # crypto lib missing at sign time — fall back
143
+ mac = hmac.new(self.private_key, message, hashlib.sha256).digest()
144
+ return b"DEV-MODE:" + mac
145
+
146
+ def verify(self, message: bytes, signature: bytes) -> bool:
147
+ """Verify a signature with the agent's public key."""
148
+ if signature.startswith(b"DEV-MODE:"):
149
+ # DEV fallback verify
150
+ expected = hmac.new(self.private_key, message,
151
+ hashlib.sha256).digest()
152
+ actual = signature[len(b"DEV-MODE:"):]
153
+ return hmac.compare_digest(expected, actual)
154
+ try:
155
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
156
+ Ed25519PublicKey)
157
+ pub = Ed25519PublicKey.from_public_bytes(self.public_key)
158
+ try:
159
+ pub.verify(signature, message)
160
+ return True
161
+ except Exception:
162
+ return False
163
+ except ImportError:
164
+ return False
165
+
166
+
167
+ def _did_key_from_pubkey(pubkey: bytes) -> str:
168
+ """Encode a 32-byte Ed25519 pubkey as a did:key identifier.
169
+
170
+ Format: did:key:z<base58btc-multibase-prefix><pubkey>
171
+ The multicodec prefix for Ed25519 is 0xed01.
172
+ """
173
+ prefixed = b"\xed\x01" + pubkey
174
+ encoded = _base58btc_encode(prefixed)
175
+ return f"did:key:z{encoded}"
176
+
177
+
178
+ def _base58btc_encode(data: bytes) -> str:
179
+ """Encode bytes in base58 with the Bitcoin alphabet."""
180
+ alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
181
+ num = int.from_bytes(data, "big")
182
+ encoded = ""
183
+ while num > 0:
184
+ num, rem = divmod(num, 58)
185
+ encoded = alphabet[rem] + encoded
186
+ n_zeros = 0
187
+ for b in data:
188
+ if b == 0:
189
+ n_zeros += 1
190
+ else:
191
+ break
192
+ return "1" * n_zeros + encoded
193
+
194
+
195
+ # ---------- default agent registry ------------------------------------
196
+
197
+ _default_agent = None
198
+
199
+
200
+ def get_default_agent() -> Ed25519AgentKey:
201
+ """Get the process-default agent key. Generates one on first call."""
202
+ global _default_agent
203
+ if _default_agent is None:
204
+ _default_agent = Ed25519AgentKey.generate()
205
+ return _default_agent
206
+
207
+
208
+ def set_default_agent(agent: Ed25519AgentKey) -> None:
209
+ """Set the process-default agent key (e.g. loaded from PEM)."""
210
+ global _default_agent
211
+ _default_agent = agent
212
+
213
+
214
+ __all__ = ["Ed25519AgentKey", "get_default_agent", "set_default_agent"]