nucleus-mcp 1.0.7 → 1.2.0
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.
- package/package.json +2 -4
- package/.github/workflows/ci.yml +0 -72
- package/.smithery/shttp/module.js +0 -3
- package/.smithery/shttp/module.js.map +0 -7
- package/.smithery/stdio/index.cjs +0 -74
- package/.smithery/stdio/index.cjs.map +0 -7
- package/CHANGELOG.md +0 -65
- package/CODE_OF_CONDUCT.md +0 -34
- package/COMPARISON.md +0 -20
- package/CONTRIBUTING.md +0 -137
- package/DEVELOPMENT.md +0 -109
- package/LICENSE +0 -21
- package/README.md +0 -319
- package/ROADMAP.md +0 -24
- package/SECURITY.md +0 -88
- package/docs/COMMUNITY_FAQ.md +0 -48
- package/docs/COMPETITIVE_ANALYSIS.md +0 -90
- package/docs/DEMO_SCRIPT.md +0 -86
- package/docs/ENTERPRISE.md +0 -169
- package/docs/FAQ.md +0 -228
- package/docs/QUICK_START.md +0 -155
- package/docs/README.md +0 -67
- package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
- package/docs/social-preview-hq.png +0 -0
- package/docs/social-preview.png +0 -0
- package/examples/basic_usage.py +0 -84
- package/package.json.real +0 -24
- package/pyproject.toml +0 -50
- package/registry-server.json +0 -20
- package/scripts/demo_governance.py +0 -109
- package/scripts/sse_bridge.py +0 -87
- package/scripts/verify_windows_paths.py +0 -48
- package/server.json +0 -20
- package/src/mcp_server_nucleus/__init__.py +0 -8583
- package/src/mcp_server_nucleus/__main__.py +0 -9
- package/src/mcp_server_nucleus/cli.py +0 -1225
- package/src/mcp_server_nucleus/commitment_core.py +0 -146
- package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
- package/src/mcp_server_nucleus/core/__init__.py +0 -6
- package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
- package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
- package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
- package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
- package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
- package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
- package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
- package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
- package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
- package/src/mcp_server_nucleus/runtime/agent.py +0 -517
- package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
- package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
- package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
- package/src/mcp_server_nucleus/runtime/auth.py +0 -98
- package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
- package/src/mcp_server_nucleus/runtime/broker.py +0 -126
- package/src/mcp_server_nucleus/runtime/budget.py +0 -153
- package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
- package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
- package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
- package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
- package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
- package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
- package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
- package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
- package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
- package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
- package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
- package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
- package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
- package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
- package/src/mcp_server_nucleus/runtime/common.py +0 -134
- package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
- package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
- package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
- package/src/mcp_server_nucleus/runtime/critic.py +0 -150
- package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
- package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
- package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
- package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
- package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
- package/src/mcp_server_nucleus/runtime/export.py +0 -64
- package/src/mcp_server_nucleus/runtime/factory.py +0 -594
- package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
- package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
- package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
- package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
- package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
- package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
- package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
- package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
- package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
- package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
- package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
- package/src/mcp_server_nucleus/runtime/installer.py +0 -81
- package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
- package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
- package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
- package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
- package/src/mcp_server_nucleus/runtime/locking.py +0 -228
- package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
- package/src/mcp_server_nucleus/runtime/memory.py +0 -158
- package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
- package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
- package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
- package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
- package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
- package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
- package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
- package/src/mcp_server_nucleus/runtime/policy.py +0 -117
- package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
- package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
- package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
- package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
- package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
- package/src/mcp_server_nucleus/runtime/registry.py +0 -82
- package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
- package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
- package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
- package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
- package/src/mcp_server_nucleus/runtime/storage.py +0 -83
- package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
- package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
- package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
- package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
- package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
- package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
- package/src/mcp_server_nucleus/runtime/team.py +0 -59
- package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
- package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
- package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
- package/src/mcp_server_nucleus/tool_tiers.py +0 -243
- package/tests/__init__.py +0 -1
- package/tests/conftest.py +0 -42
- package/tests/test_core.py +0 -235
- package/tests/test_search.py +0 -69
|
@@ -1,516 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Nucleus Runtime - Pulse Telemetry (v1.1.0 Scaffold)
|
|
3
|
-
====================================================
|
|
4
|
-
Privacy-preserving, sovereign telemetry for Nucleus MCP.
|
|
5
|
-
|
|
6
|
-
Architecture:
|
|
7
|
-
- All events stored locally in .brain/pulse/
|
|
8
|
-
- Monthly salt rotation prevents persistent identity
|
|
9
|
-
- Differential privacy via 10% decoy pulse injection
|
|
10
|
-
- `mcp pulse --view` shows exact payload (full audit)
|
|
11
|
-
- `mcp pulse --share` is opt-in upload (stub until gateway live)
|
|
12
|
-
|
|
13
|
-
References:
|
|
14
|
-
- SUPER_SOVEREIGN_MANIFEST.md (architecture)
|
|
15
|
-
- SOVEREIGN_AUDIT.md (irreversible pillars)
|
|
16
|
-
|
|
17
|
-
NOTE: This module does NOT transmit anything. It is a local-only scaffold
|
|
18
|
-
for v1.1.0. The Blinded Pulse gateway (Cloudflare Worker) is built separately.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
import os
|
|
22
|
-
import json
|
|
23
|
-
import time
|
|
24
|
-
import hashlib
|
|
25
|
-
import secrets
|
|
26
|
-
import logging
|
|
27
|
-
from pathlib import Path
|
|
28
|
-
from datetime import datetime, timezone
|
|
29
|
-
from typing import Dict, List, Optional, Any
|
|
30
|
-
from dataclasses import dataclass, field, asdict
|
|
31
|
-
|
|
32
|
-
logger = logging.getLogger("nucleus.pulse")
|
|
33
|
-
|
|
34
|
-
# ============================================================
|
|
35
|
-
# CONFIGURATION
|
|
36
|
-
# ============================================================
|
|
37
|
-
|
|
38
|
-
# Consent states
|
|
39
|
-
CONSENT_PENDING = "pending" # First run, hasn't been shown yet
|
|
40
|
-
CONSENT_ALLOWED = "allowed" # User accepted default-allow
|
|
41
|
-
CONSENT_DENIED = "denied" # User opted out
|
|
42
|
-
CONSENT_REVOKED = "revoked" # User revoked after initial allow
|
|
43
|
-
|
|
44
|
-
# Salt rotation periods (seconds)
|
|
45
|
-
MONTHLY_ROTATION = 30 * 24 * 60 * 60 # ~30 days
|
|
46
|
-
YEARLY_ROTATION = 365 * 24 * 60 * 60 # ~365 days
|
|
47
|
-
|
|
48
|
-
# Differential privacy
|
|
49
|
-
DECOY_RATIO = 0.10 # 10% of events are synthetic noise
|
|
50
|
-
|
|
51
|
-
# Event categories (Capability Hubs, not individual tools)
|
|
52
|
-
HUBS = {
|
|
53
|
-
"memory": "Engram read/write operations",
|
|
54
|
-
"filesystem": "Lock/unlock/watch operations",
|
|
55
|
-
"governance": "Policy engine interactions",
|
|
56
|
-
"sync": "Cross-platform sync events",
|
|
57
|
-
"task": "Task ledger operations",
|
|
58
|
-
"session": "Session save/resume events",
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# ============================================================
|
|
63
|
-
# DATA STRUCTURES
|
|
64
|
-
# ============================================================
|
|
65
|
-
|
|
66
|
-
@dataclass
|
|
67
|
-
class PulseEvent:
|
|
68
|
-
"""A single telemetry event. Stored locally, never transmitted without consent."""
|
|
69
|
-
hub: str # Capability hub (memory, filesystem, etc.)
|
|
70
|
-
action: str # Generic action (read, write, lock, query)
|
|
71
|
-
timestamp: str = "" # ISO 8601 UTC
|
|
72
|
-
pulse_id: str = "" # Salted, rotating anonymous ID
|
|
73
|
-
is_decoy: bool = False # True if this is a synthetic noise event
|
|
74
|
-
metadata: Dict[str, Any] = field(default_factory=dict) # Hub-specific counters
|
|
75
|
-
|
|
76
|
-
def __post_init__(self):
|
|
77
|
-
if not self.timestamp:
|
|
78
|
-
self.timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@dataclass
|
|
82
|
-
class PulseConsent:
|
|
83
|
-
"""Tracks user consent state for Pulse telemetry."""
|
|
84
|
-
state: str = CONSENT_PENDING
|
|
85
|
-
shown_at: Optional[str] = None # When the handshake was displayed
|
|
86
|
-
decided_at: Optional[str] = None # When user made a choice
|
|
87
|
-
version: str = "1.0" # Consent version (for re-consent on changes)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# ============================================================
|
|
91
|
-
# SALT MANAGEMENT (Tiered Hashing)
|
|
92
|
-
# ============================================================
|
|
93
|
-
|
|
94
|
-
class SaltManager:
|
|
95
|
-
"""
|
|
96
|
-
Manages tiered salts for anonymous pulse IDs.
|
|
97
|
-
|
|
98
|
-
Pillar 1 from SOVEREIGN_AUDIT: Tiered Hashing
|
|
99
|
-
- Monthly salt → DAU/MAU calculations
|
|
100
|
-
- Yearly salt → Cohort analysis (Q1/Q2/etc.)
|
|
101
|
-
- Individual identity is NEVER persistent
|
|
102
|
-
"""
|
|
103
|
-
|
|
104
|
-
def __init__(self, pulse_dir: Path):
|
|
105
|
-
self._pulse_dir = pulse_dir
|
|
106
|
-
self._salt_file = pulse_dir / "salts.json"
|
|
107
|
-
|
|
108
|
-
def _load_salts(self) -> Dict:
|
|
109
|
-
if self._salt_file.exists():
|
|
110
|
-
with open(self._salt_file) as f:
|
|
111
|
-
return json.load(f)
|
|
112
|
-
return {}
|
|
113
|
-
|
|
114
|
-
def _save_salts(self, data: Dict) -> None:
|
|
115
|
-
self._salt_file.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
-
with open(self._salt_file, "w") as f:
|
|
117
|
-
json.dump(data, f, indent=2)
|
|
118
|
-
|
|
119
|
-
def get_monthly_salt(self) -> str:
|
|
120
|
-
"""Get or rotate the monthly salt."""
|
|
121
|
-
salts = self._load_salts()
|
|
122
|
-
now = time.time()
|
|
123
|
-
|
|
124
|
-
monthly = salts.get("monthly", {})
|
|
125
|
-
created = monthly.get("created_at", 0)
|
|
126
|
-
|
|
127
|
-
if now - created > MONTHLY_ROTATION or not monthly.get("value"):
|
|
128
|
-
# Rotation time: snapshot aggregates before purging
|
|
129
|
-
old_salt = monthly.get("value")
|
|
130
|
-
if old_salt:
|
|
131
|
-
self._snapshot_before_rotation(salts, "monthly")
|
|
132
|
-
|
|
133
|
-
salts["monthly"] = {
|
|
134
|
-
"value": secrets.token_hex(32),
|
|
135
|
-
"created_at": now,
|
|
136
|
-
"rotates_at": now + MONTHLY_ROTATION,
|
|
137
|
-
}
|
|
138
|
-
self._save_salts(salts)
|
|
139
|
-
|
|
140
|
-
return salts["monthly"]["value"]
|
|
141
|
-
|
|
142
|
-
def get_yearly_salt(self) -> str:
|
|
143
|
-
"""Get or rotate the yearly salt (cohort analysis)."""
|
|
144
|
-
salts = self._load_salts()
|
|
145
|
-
now = time.time()
|
|
146
|
-
|
|
147
|
-
yearly = salts.get("yearly", {})
|
|
148
|
-
created = yearly.get("created_at", 0)
|
|
149
|
-
|
|
150
|
-
if now - created > YEARLY_ROTATION or not yearly.get("value"):
|
|
151
|
-
salts["yearly"] = {
|
|
152
|
-
"value": secrets.token_hex(32),
|
|
153
|
-
"created_at": now,
|
|
154
|
-
"rotates_at": now + YEARLY_ROTATION,
|
|
155
|
-
}
|
|
156
|
-
self._save_salts(salts)
|
|
157
|
-
|
|
158
|
-
return salts["yearly"]["value"]
|
|
159
|
-
|
|
160
|
-
def _snapshot_before_rotation(self, salts: Dict, tier: str) -> None:
|
|
161
|
-
"""
|
|
162
|
-
Pillar 5 from SOVEREIGN_AUDIT: Snapshot Schema.
|
|
163
|
-
Capture aggregate density before salt rotation so growth story survives.
|
|
164
|
-
"""
|
|
165
|
-
snapshots = salts.get("snapshots", [])
|
|
166
|
-
snapshots.append({
|
|
167
|
-
"tier": tier,
|
|
168
|
-
"rotated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
169
|
-
"note": "Aggregate snapshot taken before salt rotation. Raw logs purged.",
|
|
170
|
-
})
|
|
171
|
-
salts["snapshots"] = snapshots[-12] # Keep last 12 snapshots
|
|
172
|
-
|
|
173
|
-
def generate_pulse_id(self) -> str:
|
|
174
|
-
"""
|
|
175
|
-
Generate an anonymous Pulse ID using the monthly salt.
|
|
176
|
-
|
|
177
|
-
The ID is a hash of (salt + machine_id_proxy). It is NOT your machine ID.
|
|
178
|
-
It rotates every 30 days. No persistent identity can exist.
|
|
179
|
-
"""
|
|
180
|
-
salt = self.get_monthly_salt()
|
|
181
|
-
# Machine-local uniqueness without hardware identification
|
|
182
|
-
# Uses the brain directory path as a proxy (unique per project, not per user)
|
|
183
|
-
machine_proxy = str(self._pulse_dir.parent.resolve())
|
|
184
|
-
raw = f"{salt}:{machine_proxy}"
|
|
185
|
-
return hashlib.sha256(raw.encode()).hexdigest()[:16]
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
# ============================================================
|
|
189
|
-
# PULSE STORE (Local Event Storage)
|
|
190
|
-
# ============================================================
|
|
191
|
-
|
|
192
|
-
class PulseStore:
|
|
193
|
-
"""
|
|
194
|
-
Local-only storage for Pulse events.
|
|
195
|
-
|
|
196
|
-
Events are stored in .brain/pulse/events.jsonl.
|
|
197
|
-
Consent state is stored in .brain/pulse/consent.json.
|
|
198
|
-
|
|
199
|
-
This store NEVER transmits data. The `share()` method is a stub
|
|
200
|
-
that prepares a payload for the future Blinded Pulse gateway.
|
|
201
|
-
"""
|
|
202
|
-
|
|
203
|
-
def __init__(self, brain_path: Optional[Path] = None):
|
|
204
|
-
if brain_path is None:
|
|
205
|
-
brain_env = os.environ.get("NUCLEAR_BRAIN_PATH")
|
|
206
|
-
if not brain_env:
|
|
207
|
-
raise ValueError("NUCLEAR_BRAIN_PATH not set")
|
|
208
|
-
brain_path = Path(brain_env)
|
|
209
|
-
|
|
210
|
-
self._brain_path = brain_path
|
|
211
|
-
self._pulse_dir = brain_path / "pulse"
|
|
212
|
-
self._events_file = self._pulse_dir / "events.jsonl"
|
|
213
|
-
self._consent_file = self._pulse_dir / "consent.json"
|
|
214
|
-
self._salt_mgr = SaltManager(self._pulse_dir)
|
|
215
|
-
|
|
216
|
-
# Ensure directory exists
|
|
217
|
-
self._pulse_dir.mkdir(parents=True, exist_ok=True)
|
|
218
|
-
|
|
219
|
-
# ----------------------------------------------------------
|
|
220
|
-
# CONSENT MANAGEMENT (The Handshake)
|
|
221
|
-
# ----------------------------------------------------------
|
|
222
|
-
|
|
223
|
-
def get_consent(self) -> PulseConsent:
|
|
224
|
-
"""Get current consent state."""
|
|
225
|
-
if self._consent_file.exists():
|
|
226
|
-
with open(self._consent_file) as f:
|
|
227
|
-
data = json.load(f)
|
|
228
|
-
return PulseConsent(**data)
|
|
229
|
-
return PulseConsent()
|
|
230
|
-
|
|
231
|
-
def set_consent(self, state: str) -> PulseConsent:
|
|
232
|
-
"""Update consent state. Valid: allowed, denied, revoked."""
|
|
233
|
-
if state not in (CONSENT_ALLOWED, CONSENT_DENIED, CONSENT_REVOKED):
|
|
234
|
-
raise ValueError(f"Invalid consent state: {state}")
|
|
235
|
-
|
|
236
|
-
consent = self.get_consent()
|
|
237
|
-
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
238
|
-
|
|
239
|
-
if consent.state == CONSENT_PENDING:
|
|
240
|
-
consent.shown_at = now
|
|
241
|
-
|
|
242
|
-
consent.state = state
|
|
243
|
-
consent.decided_at = now
|
|
244
|
-
|
|
245
|
-
with open(self._consent_file, "w") as f:
|
|
246
|
-
json.dump(asdict(consent), f, indent=2)
|
|
247
|
-
|
|
248
|
-
logger.info(f"Pulse consent updated: {state}")
|
|
249
|
-
return consent
|
|
250
|
-
|
|
251
|
-
def needs_handshake(self) -> bool:
|
|
252
|
-
"""Check if the first-run handshake needs to be shown."""
|
|
253
|
-
return self.get_consent().state == CONSENT_PENDING
|
|
254
|
-
|
|
255
|
-
def handshake_message(self) -> str:
|
|
256
|
-
"""The transparent handshake message shown at first run."""
|
|
257
|
-
return """
|
|
258
|
-
╔══════════════════════════════════════════════════════════════╗
|
|
259
|
-
║ 🧠 Nucleus Pulse (v1.1) ║
|
|
260
|
-
╠══════════════════════════════════════════════════════════════╣
|
|
261
|
-
║ ║
|
|
262
|
-
║ Nucleus can collect anonymous usage metrics to help ║
|
|
263
|
-
║ improve the project. Here's exactly what we track: ║
|
|
264
|
-
║ ║
|
|
265
|
-
║ ✅ Which capability hubs are used (memory, filesystem...) ║
|
|
266
|
-
║ ✅ Action counts (how many reads, writes, locks) ║
|
|
267
|
-
║ ✅ Error rates (to find and fix bugs) ║
|
|
268
|
-
║ ║
|
|
269
|
-
║ 🚫 We NEVER track: file contents, paths, IP addresses, ║
|
|
270
|
-
║ machine IDs, or any personally identifiable data. ║
|
|
271
|
-
║ ║
|
|
272
|
-
║ 🔒 Your Pulse ID rotates every 30 days. No persistent ║
|
|
273
|
-
║ identity can exist. 10% of events are synthetic noise. ║
|
|
274
|
-
║ ║
|
|
275
|
-
║ 👁️ Run `mcp pulse --view` at ANY TIME to see the exact ║
|
|
276
|
-
║ data that would be shared. Full transparency. ║
|
|
277
|
-
║ ║
|
|
278
|
-
║ This is DEFAULT-ALLOW. To opt out: ║
|
|
279
|
-
║ `mcp pulse --deny` ║
|
|
280
|
-
║ ║
|
|
281
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
282
|
-
"""
|
|
283
|
-
|
|
284
|
-
# ----------------------------------------------------------
|
|
285
|
-
# EVENT RECORDING
|
|
286
|
-
# ----------------------------------------------------------
|
|
287
|
-
|
|
288
|
-
def record(self, hub: str, action: str, metadata: Dict = None) -> Optional[PulseEvent]:
|
|
289
|
-
"""
|
|
290
|
-
Record a Pulse event locally.
|
|
291
|
-
|
|
292
|
-
Returns None if telemetry is denied/revoked.
|
|
293
|
-
Injects decoy events for differential privacy.
|
|
294
|
-
"""
|
|
295
|
-
consent = self.get_consent()
|
|
296
|
-
|
|
297
|
-
# If denied or revoked, do nothing
|
|
298
|
-
if consent.state in (CONSENT_DENIED, CONSENT_REVOKED):
|
|
299
|
-
return None
|
|
300
|
-
|
|
301
|
-
# If pending, record locally but don't flag for share
|
|
302
|
-
# (events accumulate, user decides at handshake)
|
|
303
|
-
|
|
304
|
-
if hub not in HUBS:
|
|
305
|
-
logger.warning(f"Unknown hub: {hub}. Recording anyway.")
|
|
306
|
-
|
|
307
|
-
event = PulseEvent(
|
|
308
|
-
hub=hub,
|
|
309
|
-
action=action,
|
|
310
|
-
pulse_id=self._salt_mgr.generate_pulse_id(),
|
|
311
|
-
metadata=metadata or {},
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
self._append_event(event)
|
|
315
|
-
|
|
316
|
-
# Differential privacy: inject decoy with DECOY_RATIO probability
|
|
317
|
-
self._maybe_inject_decoy(hub)
|
|
318
|
-
|
|
319
|
-
return event
|
|
320
|
-
|
|
321
|
-
def _append_event(self, event: PulseEvent) -> None:
|
|
322
|
-
"""Append an event to the local JSONL store."""
|
|
323
|
-
with open(self._events_file, "a") as f:
|
|
324
|
-
f.write(json.dumps(asdict(event)) + "\n")
|
|
325
|
-
|
|
326
|
-
def _maybe_inject_decoy(self, real_hub: str) -> None:
|
|
327
|
-
"""
|
|
328
|
-
Differential Privacy: inject synthetic noise events.
|
|
329
|
-
|
|
330
|
-
Pillar from SUPER_SOVEREIGN_MANIFEST:
|
|
331
|
-
10% of events are synthetic to prevent timing/presence analysis.
|
|
332
|
-
"""
|
|
333
|
-
import random
|
|
334
|
-
if random.random() < DECOY_RATIO:
|
|
335
|
-
decoy_hub = random.choice(list(HUBS.keys()))
|
|
336
|
-
decoy_actions = ["read", "write", "query", "lock", "unlock", "sync"]
|
|
337
|
-
decoy = PulseEvent(
|
|
338
|
-
hub=decoy_hub,
|
|
339
|
-
action=random.choice(decoy_actions),
|
|
340
|
-
pulse_id=self._salt_mgr.generate_pulse_id(),
|
|
341
|
-
is_decoy=True,
|
|
342
|
-
)
|
|
343
|
-
self._append_event(decoy)
|
|
344
|
-
|
|
345
|
-
# ----------------------------------------------------------
|
|
346
|
-
# AUDIT CLI: `mcp pulse --view`
|
|
347
|
-
# ----------------------------------------------------------
|
|
348
|
-
|
|
349
|
-
def view(self, limit: int = 50) -> List[Dict]:
|
|
350
|
-
"""
|
|
351
|
-
Show the exact events stored locally.
|
|
352
|
-
|
|
353
|
-
This is the core of the Sovereign promise:
|
|
354
|
-
users can inspect every single event before sharing.
|
|
355
|
-
"""
|
|
356
|
-
if not self._events_file.exists():
|
|
357
|
-
return []
|
|
358
|
-
|
|
359
|
-
events = []
|
|
360
|
-
with open(self._events_file) as f:
|
|
361
|
-
for line in f:
|
|
362
|
-
line = line.strip()
|
|
363
|
-
if line:
|
|
364
|
-
events.append(json.loads(line))
|
|
365
|
-
|
|
366
|
-
# Return most recent first
|
|
367
|
-
return events[-limit:][::-1]
|
|
368
|
-
|
|
369
|
-
def view_summary(self) -> Dict:
|
|
370
|
-
"""
|
|
371
|
-
Get an aggregate summary of local pulse data.
|
|
372
|
-
|
|
373
|
-
This is what `mcp pulse --view` shows in the CLI.
|
|
374
|
-
"""
|
|
375
|
-
events = self.view(limit=10000)
|
|
376
|
-
|
|
377
|
-
total = len(events)
|
|
378
|
-
real_events = [e for e in events if not e.get("is_decoy")]
|
|
379
|
-
decoy_events = [e for e in events if e.get("is_decoy")]
|
|
380
|
-
|
|
381
|
-
# Hub breakdown
|
|
382
|
-
hub_counts: Dict[str, int] = {}
|
|
383
|
-
for e in real_events:
|
|
384
|
-
hub = e.get("hub", "unknown")
|
|
385
|
-
hub_counts[hub] = hub_counts.get(hub, 0) + 1
|
|
386
|
-
|
|
387
|
-
# Consent state
|
|
388
|
-
consent = self.get_consent()
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
"consent": consent.state,
|
|
392
|
-
"total_events": total,
|
|
393
|
-
"real_events": len(real_events),
|
|
394
|
-
"decoy_events": len(decoy_events),
|
|
395
|
-
"decoy_ratio": f"{len(decoy_events)/total*100:.1f}%" if total > 0 else "N/A",
|
|
396
|
-
"hub_breakdown": hub_counts,
|
|
397
|
-
"salt_rotation": self._salt_rotation_info(),
|
|
398
|
-
"sharing_enabled": consent.state == CONSENT_ALLOWED,
|
|
399
|
-
"note": "Run `mcp pulse --view` to see raw events. Nothing is hidden.",
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
def _salt_rotation_info(self) -> Dict:
|
|
403
|
-
"""Get salt rotation status for audit display."""
|
|
404
|
-
salts = self._salt_mgr._load_salts()
|
|
405
|
-
monthly = salts.get("monthly", {})
|
|
406
|
-
rotates_at = monthly.get("rotates_at", 0)
|
|
407
|
-
|
|
408
|
-
if rotates_at > 0:
|
|
409
|
-
remaining = rotates_at - time.time()
|
|
410
|
-
days_remaining = max(0, int(remaining / 86400))
|
|
411
|
-
return {
|
|
412
|
-
"next_monthly_rotation_days": days_remaining,
|
|
413
|
-
"note": "Your Pulse ID changes every 30 days. No persistent identity.",
|
|
414
|
-
}
|
|
415
|
-
return {"note": "No active salt. First event will initialize."}
|
|
416
|
-
|
|
417
|
-
# ----------------------------------------------------------
|
|
418
|
-
# SHARE (Stub — Blinded Pulse Gateway)
|
|
419
|
-
# ----------------------------------------------------------
|
|
420
|
-
|
|
421
|
-
def prepare_payload(self) -> Optional[Dict]:
|
|
422
|
-
"""
|
|
423
|
-
Prepare the share payload for the Blinded Pulse gateway.
|
|
424
|
-
|
|
425
|
-
This method:
|
|
426
|
-
1. Gathers local events
|
|
427
|
-
2. Strips any accidental PII
|
|
428
|
-
3. Returns the exact JSON that WOULD be sent
|
|
429
|
-
|
|
430
|
-
NOTE: This does NOT transmit. The gateway is not built yet.
|
|
431
|
-
|
|
432
|
-
Pillar 4 from SOVEREIGN_AUDIT: Attribution Wall
|
|
433
|
-
This payload is physically air-gapped from Scarf install data.
|
|
434
|
-
"""
|
|
435
|
-
consent = self.get_consent()
|
|
436
|
-
if consent.state != CONSENT_ALLOWED:
|
|
437
|
-
return None
|
|
438
|
-
|
|
439
|
-
events = self.view(limit=10000)
|
|
440
|
-
|
|
441
|
-
# Strip any fields that shouldn't exist (defensive)
|
|
442
|
-
sanitized = []
|
|
443
|
-
allowed_fields = {"hub", "action", "timestamp", "pulse_id", "is_decoy", "metadata"}
|
|
444
|
-
for event in events:
|
|
445
|
-
clean = {k: v for k, v in event.items() if k in allowed_fields}
|
|
446
|
-
# Double-check: metadata must not contain paths or identifiers
|
|
447
|
-
if "metadata" in clean:
|
|
448
|
-
clean["metadata"] = {
|
|
449
|
-
k: v for k, v in clean["metadata"].items()
|
|
450
|
-
if k in {"count", "duration_ms", "error_count", "version"}
|
|
451
|
-
}
|
|
452
|
-
sanitized.append(clean)
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
"schema_version": "1.0",
|
|
456
|
-
"pulse_id": self._salt_mgr.generate_pulse_id(),
|
|
457
|
-
"event_count": len(sanitized),
|
|
458
|
-
"events": sanitized,
|
|
459
|
-
"transmission_note": "STUB — Blinded Pulse gateway not yet deployed.",
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
# ----------------------------------------------------------
|
|
463
|
-
# DATA MANAGEMENT
|
|
464
|
-
# ----------------------------------------------------------
|
|
465
|
-
|
|
466
|
-
def purge(self) -> str:
|
|
467
|
-
"""
|
|
468
|
-
Delete all local pulse data. Irreversible.
|
|
469
|
-
|
|
470
|
-
This is the user's nuclear option. We respect it unconditionally.
|
|
471
|
-
"""
|
|
472
|
-
if self._events_file.exists():
|
|
473
|
-
self._events_file.unlink()
|
|
474
|
-
|
|
475
|
-
# Reset consent to denied
|
|
476
|
-
self.set_consent(CONSENT_DENIED)
|
|
477
|
-
|
|
478
|
-
return "All Pulse data purged. Consent set to denied."
|
|
479
|
-
|
|
480
|
-
def event_count(self) -> int:
|
|
481
|
-
"""Count total local events."""
|
|
482
|
-
if not self._events_file.exists():
|
|
483
|
-
return 0
|
|
484
|
-
with open(self._events_file) as f:
|
|
485
|
-
return sum(1 for line in f if line.strip())
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
# ============================================================
|
|
489
|
-
# MODULE-LEVEL CONVENIENCE
|
|
490
|
-
# ============================================================
|
|
491
|
-
|
|
492
|
-
_store: Optional[PulseStore] = None
|
|
493
|
-
|
|
494
|
-
def get_pulse_store() -> PulseStore:
|
|
495
|
-
"""Get or create the singleton PulseStore."""
|
|
496
|
-
global _store
|
|
497
|
-
if _store is None:
|
|
498
|
-
try:
|
|
499
|
-
_store = PulseStore()
|
|
500
|
-
except ValueError:
|
|
501
|
-
logger.warning("PulseStore: NUCLEAR_BRAIN_PATH not set. Pulse disabled.")
|
|
502
|
-
return None
|
|
503
|
-
return _store
|
|
504
|
-
|
|
505
|
-
def pulse_record(hub: str, action: str, **metadata) -> None:
|
|
506
|
-
"""Quick-record a pulse event. Safe to call even if Pulse is disabled."""
|
|
507
|
-
store = get_pulse_store()
|
|
508
|
-
if store:
|
|
509
|
-
store.record(hub, action, metadata if metadata else None)
|
|
510
|
-
|
|
511
|
-
def pulse_view() -> Dict:
|
|
512
|
-
"""Quick-view pulse summary."""
|
|
513
|
-
store = get_pulse_store()
|
|
514
|
-
if store:
|
|
515
|
-
return store.view_summary()
|
|
516
|
-
return {"error": "Pulse not initialized", "note": "Set NUCLEAR_BRAIN_PATH"}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
RegistryClient: The Catalog Browser.
|
|
3
|
-
Fetches and searches the global directory of available Agents.
|
|
4
|
-
|
|
5
|
-
Strategic Role:
|
|
6
|
-
- DISCOVERY: Where do I find agents?
|
|
7
|
-
- TRUST: The Index is the first point of truth (though Verify Keymaster is final).
|
|
8
|
-
- VERSIONING: Knows what the "latest" version is.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import logging
|
|
13
|
-
import urllib.request
|
|
14
|
-
from typing import List, Optional
|
|
15
|
-
from pydantic import BaseModel
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger("REGISTRY")
|
|
18
|
-
|
|
19
|
-
class RegistryEntry(BaseModel):
|
|
20
|
-
id: str
|
|
21
|
-
name: str
|
|
22
|
-
description: str
|
|
23
|
-
latest_version: str
|
|
24
|
-
repo_url: str
|
|
25
|
-
tags: List[str] = []
|
|
26
|
-
|
|
27
|
-
class RegistryClient:
|
|
28
|
-
def __init__(self, registry_url: str = "https://registry.nucleus.dev/index.json"):
|
|
29
|
-
self.registry_url = registry_url
|
|
30
|
-
self._cache: List[RegistryEntry] = []
|
|
31
|
-
|
|
32
|
-
def fetch_index(self) -> List[RegistryEntry]:
|
|
33
|
-
"""Fetch the latest index from the remote registry."""
|
|
34
|
-
logger.info(f"Fetching registry from {self.registry_url}...")
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
with urllib.request.urlopen(self.registry_url) as response:
|
|
38
|
-
data = json.loads(response.read().decode())
|
|
39
|
-
|
|
40
|
-
entries = []
|
|
41
|
-
for item in data.get("agents", []):
|
|
42
|
-
entries.append(RegistryEntry(**item))
|
|
43
|
-
|
|
44
|
-
self._cache = entries
|
|
45
|
-
logger.info(f"✅ Fetched {len(entries)} agents.")
|
|
46
|
-
return entries
|
|
47
|
-
|
|
48
|
-
except Exception as e:
|
|
49
|
-
logger.error(f"❌ Failed to fetch registry: {e}")
|
|
50
|
-
raise e
|
|
51
|
-
|
|
52
|
-
def search(self, query: str) -> List[RegistryEntry]:
|
|
53
|
-
"""Search for agents by name, id, or tag."""
|
|
54
|
-
if not self._cache:
|
|
55
|
-
# Auto-fetch if empty? Or just return empty.
|
|
56
|
-
# In Phase 57 simulation, let's assume cache is primed or we try to prime.
|
|
57
|
-
try:
|
|
58
|
-
self.fetch_index()
|
|
59
|
-
except Exception:
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
|
-
query = query.lower()
|
|
63
|
-
results = []
|
|
64
|
-
|
|
65
|
-
for entry in self._cache:
|
|
66
|
-
if (query in entry.name.lower() or
|
|
67
|
-
query in entry.id.lower() or
|
|
68
|
-
query in entry.description.lower() or
|
|
69
|
-
any(query in tag.lower() for tag in entry.tags)):
|
|
70
|
-
results.append(entry)
|
|
71
|
-
|
|
72
|
-
return results
|
|
73
|
-
|
|
74
|
-
def get_entry(self, agent_id: str) -> Optional[RegistryEntry]:
|
|
75
|
-
"""Get a specific agent entry."""
|
|
76
|
-
if not self._cache:
|
|
77
|
-
self.fetch_index()
|
|
78
|
-
|
|
79
|
-
for entry in self._cache:
|
|
80
|
-
if entry.id == agent_id:
|
|
81
|
-
return entry
|
|
82
|
-
return None
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import os
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
from typing import Dict, List, Any
|
|
6
|
-
import urllib.request
|
|
7
|
-
import urllib.error
|
|
8
|
-
|
|
9
|
-
# Configure logger
|
|
10
|
-
logger = logging.getLogger("nucleus.render")
|
|
11
|
-
|
|
12
|
-
class RenderOps:
|
|
13
|
-
"""Operations for Render.com API."""
|
|
14
|
-
|
|
15
|
-
def __init__(self, api_key: str = None):
|
|
16
|
-
self.api_key = api_key or os.environ.get("RENDER_API_KEY")
|
|
17
|
-
self.base_url = "https://api.render.com/v1"
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def is_available(self) -> bool:
|
|
21
|
-
"""Check if API key is present."""
|
|
22
|
-
return bool(self.api_key)
|
|
23
|
-
|
|
24
|
-
def list_services(self) -> List[Dict[str, Any]]:
|
|
25
|
-
"""List all services."""
|
|
26
|
-
if not self.is_available:
|
|
27
|
-
return self._get_mock_services()
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
req = urllib.request.Request(
|
|
31
|
-
f"{self.base_url}/services?limit=20",
|
|
32
|
-
headers={
|
|
33
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
34
|
-
"Accept": "application/json"
|
|
35
|
-
}
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
with urllib.request.urlopen(req) as response:
|
|
39
|
-
if response.status != 200:
|
|
40
|
-
raise Exception(f"API returned {response.status}")
|
|
41
|
-
|
|
42
|
-
data = json.loads(response.read().decode('utf-8'))
|
|
43
|
-
return data
|
|
44
|
-
|
|
45
|
-
except Exception as e:
|
|
46
|
-
logger.warning(f"Render API failed: {e}. Falling back to mock.")
|
|
47
|
-
return self._get_mock_services(error=str(e))
|
|
48
|
-
|
|
49
|
-
def _get_mock_services(self, error: str = None) -> List[Dict[str, Any]]:
|
|
50
|
-
"""Return mock services for testing/verification."""
|
|
51
|
-
mock_data = [
|
|
52
|
-
{
|
|
53
|
-
"service": {
|
|
54
|
-
"id": "srv-mock-1",
|
|
55
|
-
"name": "nucleus-backend",
|
|
56
|
-
"type": "web_service",
|
|
57
|
-
"repo": "https://github.com/lokeshgarg/ai-mvp-backend",
|
|
58
|
-
"branch": "main"
|
|
59
|
-
},
|
|
60
|
-
"cursor": "c1"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"service": {
|
|
64
|
-
"id": "srv-mock-2",
|
|
65
|
-
"name": "gentlequest-app",
|
|
66
|
-
"type": "static_site",
|
|
67
|
-
"repo": "https://github.com/lokeshgarg/gentlequest",
|
|
68
|
-
"branch": "production"
|
|
69
|
-
},
|
|
70
|
-
"cursor": "c2"
|
|
71
|
-
}
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
# Wrap in expected API format if needed, but simple list is fine for internal consumption
|
|
75
|
-
return {
|
|
76
|
-
"mock": True,
|
|
77
|
-
"message": "Render API Key not found or call failed. Showing MOCK data.",
|
|
78
|
-
"error_detail": error,
|
|
79
|
-
"items": mock_data
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
def get_render_ops():
|
|
83
|
-
return RenderOps()
|