novyx 0.1.0__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.
- novyx-0.1.0/PKG-INFO +10 -0
- novyx-0.1.0/README.md +44 -0
- novyx-0.1.0/novyx/__init__.py +9 -0
- novyx-0.1.0/novyx/auth.py +20 -0
- novyx-0.1.0/novyx/client.py +261 -0
- novyx-0.1.0/novyx/exceptions.py +10 -0
- novyx-0.1.0/novyx/telemetry.py +56 -0
- novyx-0.1.0/novyx.egg-info/PKG-INFO +10 -0
- novyx-0.1.0/novyx.egg-info/SOURCES.txt +17 -0
- novyx-0.1.0/novyx.egg-info/dependency_links.txt +1 -0
- novyx-0.1.0/novyx.egg-info/requires.txt +2 -0
- novyx-0.1.0/novyx.egg-info/top_level.txt +1 -0
- novyx-0.1.0/setup.cfg +4 -0
- novyx-0.1.0/setup.py +15 -0
novyx-0.1.0/PKG-INFO
ADDED
novyx-0.1.0/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Novyx SDK
|
|
2
|
+
|
|
3
|
+
Unified SDK for Novyx Core + RAM + Sentinel.
|
|
4
|
+
|
|
5
|
+
## Install (local)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e ./sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from novyx import Novyx
|
|
15
|
+
|
|
16
|
+
nx = Novyx(
|
|
17
|
+
api_key="nram_...",
|
|
18
|
+
api_url="https://novyx-ram-api.fly.dev",
|
|
19
|
+
sentinel_url="https://novyx-sentinel.fly.dev",
|
|
20
|
+
strict=False,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
result = nx.remember("User prefers dark mode", tags=["ui", "preference"])
|
|
24
|
+
memories = nx.recall("UI preferences", limit=5)
|
|
25
|
+
nx.act("send_email", {"to": "user@example.com"})
|
|
26
|
+
nx.rollback("2025-01-30T14:00:00Z")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Four Verbs
|
|
30
|
+
|
|
31
|
+
- `remember(content, tags=[])` → store memory with cryptographic hash
|
|
32
|
+
- `recall(query, limit=5)` → semantic search (uses `q` param)
|
|
33
|
+
- `act(action_name, params)` → Sentinel policy check before action
|
|
34
|
+
- `rollback(target)` → Magic Rollback™ (timestamp or artifact id)
|
|
35
|
+
|
|
36
|
+
## Enterprise Telemetry
|
|
37
|
+
|
|
38
|
+
Set `NOVYX_TIER=enterprise` to enable audit pulse logging to the Core audit trail.
|
|
39
|
+
|
|
40
|
+
## Test Drive
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python sdk/test_drive.py
|
|
44
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from extensions.novyx_ram.sdk import NovyxRAMClient, AuthenticationError, NovyxRAMError
|
|
2
|
+
|
|
3
|
+
from .exceptions import NovyxAuthError, NovyxStorageError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_api_key(api_key: str, api_url: str) -> None:
|
|
7
|
+
"""
|
|
8
|
+
Validate RAM API key by calling an authenticated endpoint.
|
|
9
|
+
|
|
10
|
+
Raises:
|
|
11
|
+
NovyxAuthError: If the API key is invalid.
|
|
12
|
+
NovyxStorageError: If the API is unreachable.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
client = NovyxRAMClient(api_key=api_key, base_url=api_url)
|
|
16
|
+
_ = client.stats()
|
|
17
|
+
except AuthenticationError as exc:
|
|
18
|
+
raise NovyxAuthError("Invalid Novyx API key.") from exc
|
|
19
|
+
except NovyxRAMError as exc:
|
|
20
|
+
raise NovyxStorageError(str(exc)) from exc
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from extensions.novyx_ram.sdk import (
|
|
11
|
+
NovyxRAMClient,
|
|
12
|
+
NovyxRAMError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
)
|
|
15
|
+
from sentinel.circuit_breaker import PolicyEvaluator, EvaluationMode
|
|
16
|
+
from sentinel.trace_models import SentinelTraceStep, StepType, PolicyStatus
|
|
17
|
+
from sentinel.rollback_analyzer import RollbackAnalyzer
|
|
18
|
+
from sentinel.surgical_rollback import SurgicalRollback
|
|
19
|
+
from security.forensic_rollback import ForensicRollback
|
|
20
|
+
|
|
21
|
+
from .auth import validate_api_key
|
|
22
|
+
from .exceptions import NovyxAuthError, NovyxSecurityError, NovyxStorageError
|
|
23
|
+
from .telemetry import TelemetryLogger
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Novyx:
|
|
27
|
+
"""
|
|
28
|
+
Unified SDK interface for Novyx Core + RAM + Sentinel.
|
|
29
|
+
|
|
30
|
+
Four verbs:
|
|
31
|
+
- remember
|
|
32
|
+
- recall
|
|
33
|
+
- act
|
|
34
|
+
- rollback
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
api_key: str,
|
|
40
|
+
api_url: str = "https://novyx-ram-api.fly.dev",
|
|
41
|
+
sentinel_url: str = "https://novyx-sentinel.fly.dev",
|
|
42
|
+
session_id: Optional[str] = None,
|
|
43
|
+
strict: bool = False,
|
|
44
|
+
):
|
|
45
|
+
self.api_key = api_key
|
|
46
|
+
self.api_url = api_url.rstrip("/")
|
|
47
|
+
self.sentinel_url = sentinel_url.rstrip("/")
|
|
48
|
+
self.session_id = session_id or f"session_{uuid.uuid4()}"
|
|
49
|
+
self.strict = strict
|
|
50
|
+
|
|
51
|
+
validate_api_key(api_key=self.api_key, api_url=self.api_url)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
self.ram = NovyxRAMClient(api_key=self.api_key, base_url=self.api_url)
|
|
55
|
+
except AuthenticationError as exc:
|
|
56
|
+
raise NovyxAuthError("Invalid Novyx API key.") from exc
|
|
57
|
+
except NovyxRAMError as exc:
|
|
58
|
+
raise NovyxStorageError(str(exc)) from exc
|
|
59
|
+
|
|
60
|
+
self.telemetry = TelemetryLogger(session_id=self.session_id)
|
|
61
|
+
self.policy_evaluator = PolicyEvaluator(mode=EvaluationMode.ENFORCEMENT)
|
|
62
|
+
|
|
63
|
+
self._memory_count = 0
|
|
64
|
+
self._action_count = 0
|
|
65
|
+
|
|
66
|
+
def _now_iso(self) -> str:
|
|
67
|
+
return datetime.now(timezone.utc).isoformat()
|
|
68
|
+
|
|
69
|
+
def _hash_content(self, content: str, tags: List[str]) -> str:
|
|
70
|
+
payload = {
|
|
71
|
+
"content": content,
|
|
72
|
+
"tags": tags,
|
|
73
|
+
"session_id": self.session_id,
|
|
74
|
+
}
|
|
75
|
+
digest = hashlib.sha256(
|
|
76
|
+
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
77
|
+
).hexdigest()
|
|
78
|
+
return f"urn:sha256:{digest}"
|
|
79
|
+
|
|
80
|
+
def _forge_debt_warning(self) -> None:
|
|
81
|
+
if not self.strict:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if self._action_count == 0:
|
|
85
|
+
ratio = f"{self._memory_count}:0"
|
|
86
|
+
else:
|
|
87
|
+
ratio = f"{self._memory_count}:{self._action_count}"
|
|
88
|
+
|
|
89
|
+
print(
|
|
90
|
+
"Warning: "
|
|
91
|
+
f"{self._memory_count} memories stored, "
|
|
92
|
+
f"{self._action_count} actions taken. "
|
|
93
|
+
f"Consumption debt ratio: {ratio}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def remember(self, content: str, tags: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
97
|
+
tags = tags or []
|
|
98
|
+
content_hash = self._hash_content(content, tags)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
result = self.ram.store(
|
|
102
|
+
observation=content,
|
|
103
|
+
tags=tags,
|
|
104
|
+
context=f"content_hash={content_hash}",
|
|
105
|
+
agent_id=self.session_id,
|
|
106
|
+
)
|
|
107
|
+
except NovyxRAMError as exc:
|
|
108
|
+
raise NovyxStorageError(str(exc)) from exc
|
|
109
|
+
|
|
110
|
+
self._memory_count += 1
|
|
111
|
+
self.telemetry.log_pulse(
|
|
112
|
+
"remember",
|
|
113
|
+
{
|
|
114
|
+
"session_id": self.session_id,
|
|
115
|
+
"memory_id": result.uuid,
|
|
116
|
+
"hash": content_hash,
|
|
117
|
+
"tags": tags,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
self._forge_debt_warning()
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"id": result.uuid,
|
|
124
|
+
"hash": content_hash,
|
|
125
|
+
"stored_at": self._now_iso(),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def recall(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
|
|
129
|
+
try:
|
|
130
|
+
search_result = self.ram.search(query=query, limit=limit)
|
|
131
|
+
except NovyxRAMError as exc:
|
|
132
|
+
raise NovyxStorageError(str(exc)) from exc
|
|
133
|
+
|
|
134
|
+
retrieved_at = self._now_iso()
|
|
135
|
+
cleaned = []
|
|
136
|
+
for mem in search_result.memories:
|
|
137
|
+
cleaned.append(
|
|
138
|
+
{
|
|
139
|
+
"uuid": mem.uuid,
|
|
140
|
+
"observation": mem.observation,
|
|
141
|
+
"context": mem.context,
|
|
142
|
+
"tags": mem.tags,
|
|
143
|
+
"agent_id": mem.agent_id,
|
|
144
|
+
"created_at": mem.created_at,
|
|
145
|
+
"score": mem.score,
|
|
146
|
+
"query": query,
|
|
147
|
+
"retrieved_at": retrieved_at,
|
|
148
|
+
"session_id": self.session_id,
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
self.telemetry.log_pulse(
|
|
153
|
+
"recall",
|
|
154
|
+
{
|
|
155
|
+
"session_id": self.session_id,
|
|
156
|
+
"query": query,
|
|
157
|
+
"limit": limit,
|
|
158
|
+
"results": len(cleaned),
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
return cleaned
|
|
162
|
+
|
|
163
|
+
def act(self, action_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
164
|
+
self._check_sentinel()
|
|
165
|
+
content = json.dumps({"action": action_name, "params": params}, sort_keys=True)
|
|
166
|
+
integrity_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
167
|
+
chain_hash = hashlib.sha256(
|
|
168
|
+
(integrity_hash + ("0" * 64)).encode("utf-8")
|
|
169
|
+
).hexdigest()
|
|
170
|
+
|
|
171
|
+
step = SentinelTraceStep(
|
|
172
|
+
trace_id=f"urn:uuid:{self.session_id}",
|
|
173
|
+
step_index=self._action_count,
|
|
174
|
+
step_type=StepType.ACTION,
|
|
175
|
+
content=content,
|
|
176
|
+
policy_status=PolicyStatus.SKIPPED,
|
|
177
|
+
integrity_hash=integrity_hash,
|
|
178
|
+
integrity_chain_hash=chain_hash,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
evaluation = self.policy_evaluator.evaluate_step(step)
|
|
182
|
+
if evaluation.policy_status == PolicyStatus.FAILED:
|
|
183
|
+
violation = evaluation.violations[0]
|
|
184
|
+
raise NovyxSecurityError(
|
|
185
|
+
f"{violation.policy_name}: {violation.reason}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self._action_count += 1
|
|
189
|
+
self.telemetry.log_pulse(
|
|
190
|
+
"act",
|
|
191
|
+
{
|
|
192
|
+
"session_id": self.session_id,
|
|
193
|
+
"action": action_name,
|
|
194
|
+
"params": params,
|
|
195
|
+
"policy_status": evaluation.policy_status.value,
|
|
196
|
+
"risk_score": evaluation.risk_score,
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"action": action_name,
|
|
202
|
+
"status": "approved",
|
|
203
|
+
"policy_status": evaluation.policy_status.value,
|
|
204
|
+
"risk_score": evaluation.risk_score,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
def _check_sentinel(self) -> None:
|
|
208
|
+
try:
|
|
209
|
+
resp = requests.get(f"{self.sentinel_url}/health", timeout=5)
|
|
210
|
+
except requests.RequestException as exc:
|
|
211
|
+
raise NovyxSecurityError("Sentinel is unreachable.") from exc
|
|
212
|
+
|
|
213
|
+
if resp.status_code != 200:
|
|
214
|
+
raise NovyxSecurityError(
|
|
215
|
+
f"Sentinel integrity check failed (status {resp.status_code})."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def rollback(self, target: str) -> Dict[str, Any]:
|
|
219
|
+
memory_dir = os.getenv("NOVYX_MEMORY_DIR", "memory")
|
|
220
|
+
analyzer = RollbackAnalyzer(memory_dir=memory_dir)
|
|
221
|
+
rollback_engine = ForensicRollback(memory_dir=memory_dir)
|
|
222
|
+
surgical = SurgicalRollback(memory_dir=memory_dir, rollback=rollback_engine)
|
|
223
|
+
|
|
224
|
+
suggestion = None
|
|
225
|
+
if "T" in target and "Z" in target:
|
|
226
|
+
impact = analyzer.calculate_impact(target)
|
|
227
|
+
result = rollback_engine.execute_rollback(target)
|
|
228
|
+
suggestion = impact.get("suggestion") or "Rollback executed"
|
|
229
|
+
rolled_back = result.get("artifacts_restored", [])
|
|
230
|
+
preserved = result.get("artifacts_quarantined", [])
|
|
231
|
+
else:
|
|
232
|
+
# Treat as artifact_id; rollback to latest green point with dependencies
|
|
233
|
+
points = rollback_engine.list_rollback_points()
|
|
234
|
+
if not points:
|
|
235
|
+
raise NovyxStorageError("No rollback points available.")
|
|
236
|
+
latest = points[-1]
|
|
237
|
+
impact = analyzer.calculate_impact(latest.timestamp)
|
|
238
|
+
suggestion = impact.get("suggestion") or "Surgical rollback executed"
|
|
239
|
+
result = surgical.rollback_artifacts(
|
|
240
|
+
[target],
|
|
241
|
+
latest.timestamp,
|
|
242
|
+
include_dependencies=True,
|
|
243
|
+
dry_run=False,
|
|
244
|
+
).to_dict()
|
|
245
|
+
rolled_back = result.get("artifacts_rolled_back", [])
|
|
246
|
+
preserved = result.get("artifacts_failed", [])
|
|
247
|
+
|
|
248
|
+
self.telemetry.log_pulse(
|
|
249
|
+
"rollback",
|
|
250
|
+
{
|
|
251
|
+
"session_id": self.session_id,
|
|
252
|
+
"target": target,
|
|
253
|
+
"suggestion": suggestion,
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"rolled_back": rolled_back,
|
|
259
|
+
"preserved": preserved,
|
|
260
|
+
"suggestion": suggestion,
|
|
261
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class NovyxAuthError(Exception):
|
|
2
|
+
"""Raised when API key authentication fails."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NovyxSecurityError(Exception):
|
|
6
|
+
"""Raised when Sentinel blocks an action or integrity check fails."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NovyxStorageError(Exception):
|
|
10
|
+
"""Raised when RAM or Core storage operations fail."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any, Union, Optional
|
|
7
|
+
|
|
8
|
+
from security.audit_trail import AuditTrail, OperationType
|
|
9
|
+
|
|
10
|
+
from .exceptions import NovyxStorageError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TelemetryLogger:
|
|
14
|
+
"""
|
|
15
|
+
Enterprise-only audit logging for SDK usage.
|
|
16
|
+
|
|
17
|
+
Logs a Pulse event for each SDK call when NOVYX_TIER=enterprise.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, session_id: str, memory_dir: Optional[Union[str, Path]] = None):
|
|
21
|
+
self.session_id = session_id
|
|
22
|
+
self.tier = os.getenv("NOVYX_TIER", "").lower()
|
|
23
|
+
self.memory_dir = Path(memory_dir or os.getenv("NOVYX_MEMORY_DIR", "memory"))
|
|
24
|
+
self.audit_dir = self.memory_dir / ".audit"
|
|
25
|
+
self._audit = None
|
|
26
|
+
|
|
27
|
+
if self.tier == "enterprise":
|
|
28
|
+
try:
|
|
29
|
+
self._audit = AuditTrail(self.audit_dir, verify_on_load=False)
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
raise NovyxStorageError(f"Failed to initialize audit trail: {exc}") from exc
|
|
32
|
+
|
|
33
|
+
def _hash_payload(self, payload: Dict[str, Any]) -> str:
|
|
34
|
+
content = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
35
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
36
|
+
|
|
37
|
+
def log_pulse(self, verb: str, payload: Dict[str, Any]) -> None:
|
|
38
|
+
if self.tier != "enterprise":
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if not self._audit:
|
|
42
|
+
raise NovyxStorageError("Audit trail not initialized.")
|
|
43
|
+
|
|
44
|
+
content_hash = self._hash_payload(payload)
|
|
45
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
46
|
+
self._audit.log(
|
|
47
|
+
operation=OperationType.ACCESS.value,
|
|
48
|
+
agent_id=self.session_id,
|
|
49
|
+
artifact_id=f"pulse:{verb}",
|
|
50
|
+
content_hash=content_hash,
|
|
51
|
+
metadata={
|
|
52
|
+
"verb": verb,
|
|
53
|
+
"timestamp": timestamp,
|
|
54
|
+
"payload": payload,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
./novyx/__init__.py
|
|
4
|
+
./novyx/auth.py
|
|
5
|
+
./novyx/client.py
|
|
6
|
+
./novyx/exceptions.py
|
|
7
|
+
./novyx/telemetry.py
|
|
8
|
+
novyx/__init__.py
|
|
9
|
+
novyx/auth.py
|
|
10
|
+
novyx/client.py
|
|
11
|
+
novyx/exceptions.py
|
|
12
|
+
novyx/telemetry.py
|
|
13
|
+
novyx.egg-info/PKG-INFO
|
|
14
|
+
novyx.egg-info/SOURCES.txt
|
|
15
|
+
novyx.egg-info/dependency_links.txt
|
|
16
|
+
novyx.egg-info/requires.txt
|
|
17
|
+
novyx.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
novyx
|
novyx-0.1.0/setup.cfg
ADDED
novyx-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
name="novyx",
|
|
6
|
+
version="0.1.0",
|
|
7
|
+
description="Unified Novyx SDK (Core + RAM + Sentinel)",
|
|
8
|
+
package_dir={"": "."},
|
|
9
|
+
packages=find_packages("."),
|
|
10
|
+
install_requires=[
|
|
11
|
+
"requests",
|
|
12
|
+
"pydantic",
|
|
13
|
+
],
|
|
14
|
+
python_requires=">=3.9",
|
|
15
|
+
)
|