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.
Files changed (135) hide show
  1. package/package.json +2 -4
  2. package/.github/workflows/ci.yml +0 -72
  3. package/.smithery/shttp/module.js +0 -3
  4. package/.smithery/shttp/module.js.map +0 -7
  5. package/.smithery/stdio/index.cjs +0 -74
  6. package/.smithery/stdio/index.cjs.map +0 -7
  7. package/CHANGELOG.md +0 -65
  8. package/CODE_OF_CONDUCT.md +0 -34
  9. package/COMPARISON.md +0 -20
  10. package/CONTRIBUTING.md +0 -137
  11. package/DEVELOPMENT.md +0 -109
  12. package/LICENSE +0 -21
  13. package/README.md +0 -319
  14. package/ROADMAP.md +0 -24
  15. package/SECURITY.md +0 -88
  16. package/docs/COMMUNITY_FAQ.md +0 -48
  17. package/docs/COMPETITIVE_ANALYSIS.md +0 -90
  18. package/docs/DEMO_SCRIPT.md +0 -86
  19. package/docs/ENTERPRISE.md +0 -169
  20. package/docs/FAQ.md +0 -228
  21. package/docs/QUICK_START.md +0 -155
  22. package/docs/README.md +0 -67
  23. package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
  24. package/docs/social-preview-hq.png +0 -0
  25. package/docs/social-preview.png +0 -0
  26. package/examples/basic_usage.py +0 -84
  27. package/package.json.real +0 -24
  28. package/pyproject.toml +0 -50
  29. package/registry-server.json +0 -20
  30. package/scripts/demo_governance.py +0 -109
  31. package/scripts/sse_bridge.py +0 -87
  32. package/scripts/verify_windows_paths.py +0 -48
  33. package/server.json +0 -20
  34. package/src/mcp_server_nucleus/__init__.py +0 -8583
  35. package/src/mcp_server_nucleus/__main__.py +0 -9
  36. package/src/mcp_server_nucleus/cli.py +0 -1225
  37. package/src/mcp_server_nucleus/commitment_core.py +0 -146
  38. package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
  39. package/src/mcp_server_nucleus/core/__init__.py +0 -6
  40. package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
  41. package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
  42. package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
  43. package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
  44. package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
  45. package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
  46. package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
  47. package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
  48. package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
  49. package/src/mcp_server_nucleus/runtime/agent.py +0 -517
  50. package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
  51. package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
  52. package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
  53. package/src/mcp_server_nucleus/runtime/auth.py +0 -98
  54. package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
  55. package/src/mcp_server_nucleus/runtime/broker.py +0 -126
  56. package/src/mcp_server_nucleus/runtime/budget.py +0 -153
  57. package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
  58. package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
  59. package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
  60. package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
  61. package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
  62. package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
  63. package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
  64. package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
  65. package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
  66. package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
  67. package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
  68. package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
  69. package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
  70. package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
  71. package/src/mcp_server_nucleus/runtime/common.py +0 -134
  72. package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
  73. package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
  74. package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
  75. package/src/mcp_server_nucleus/runtime/critic.py +0 -150
  76. package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
  77. package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
  78. package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
  79. package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
  80. package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
  81. package/src/mcp_server_nucleus/runtime/export.py +0 -64
  82. package/src/mcp_server_nucleus/runtime/factory.py +0 -594
  83. package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
  84. package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
  85. package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
  86. package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
  87. package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
  88. package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
  89. package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
  90. package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
  91. package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
  92. package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
  93. package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
  94. package/src/mcp_server_nucleus/runtime/installer.py +0 -81
  95. package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
  96. package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
  97. package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
  98. package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
  99. package/src/mcp_server_nucleus/runtime/locking.py +0 -228
  100. package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
  101. package/src/mcp_server_nucleus/runtime/memory.py +0 -158
  102. package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
  103. package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
  104. package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
  105. package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
  106. package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
  107. package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
  108. package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
  109. package/src/mcp_server_nucleus/runtime/policy.py +0 -117
  110. package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
  111. package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
  112. package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
  113. package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
  114. package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
  115. package/src/mcp_server_nucleus/runtime/registry.py +0 -82
  116. package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
  117. package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
  118. package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
  119. package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
  120. package/src/mcp_server_nucleus/runtime/storage.py +0 -83
  121. package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
  122. package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
  123. package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
  124. package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
  125. package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
  126. package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
  127. package/src/mcp_server_nucleus/runtime/team.py +0 -59
  128. package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
  129. package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
  130. package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
  131. package/src/mcp_server_nucleus/tool_tiers.py +0 -243
  132. package/tests/__init__.py +0 -1
  133. package/tests/conftest.py +0 -42
  134. package/tests/test_core.py +0 -235
  135. 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()