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,246 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
"""
|
|
3
|
-
Strategic Hooks for Nucleus Daemon.
|
|
4
|
-
The "Billion Dollar Extensions" for Network Effects, Identity, and Scale.
|
|
5
|
-
|
|
6
|
-
Strategic Role:
|
|
7
|
-
- Turn the Daemon into a Platform (App Store).
|
|
8
|
-
- Turn the Daemon into a Global Node (Insight Exchange).
|
|
9
|
-
- Deepen Host Integration (Intimacy).
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import logging
|
|
14
|
-
import time
|
|
15
|
-
import uuid
|
|
16
|
-
import hashlib
|
|
17
|
-
from typing import List, Optional
|
|
18
|
-
from dataclasses import dataclass, asdict
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
# --- CRYPTO UPGRADE (Phase 60: Gates Fix) ---
|
|
22
|
-
try:
|
|
23
|
-
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
24
|
-
from cryptography.hazmat.primitives import serialization
|
|
25
|
-
CRYPTO_AVAILABLE = True
|
|
26
|
-
except ImportError:
|
|
27
|
-
CRYPTO_AVAILABLE = False
|
|
28
|
-
|
|
29
|
-
logger = logging.getLogger(__name__)
|
|
30
|
-
|
|
31
|
-
# --- 1. The App Store Hook (AgentManifest) ---
|
|
32
|
-
@dataclass
|
|
33
|
-
class AgentManifest:
|
|
34
|
-
"""Schema for a Tradeable Agent (The App Store Unit)."""
|
|
35
|
-
id: str
|
|
36
|
-
name: str
|
|
37
|
-
version: str
|
|
38
|
-
author_did: str # Decentralized ID of creator
|
|
39
|
-
capabilities: List[str]
|
|
40
|
-
lifecycle_state: str = "active" # active, archived, tombstone
|
|
41
|
-
price_per_invocation_usd: float = 0.0
|
|
42
|
-
hash: str = ""
|
|
43
|
-
signature: str = ""
|
|
44
|
-
|
|
45
|
-
def sign(self, identity_key: 'IdentityKey'):
|
|
46
|
-
"""Sign the manifest to prove authorship."""
|
|
47
|
-
# Canonicalize the data
|
|
48
|
-
payload = f"{self.id}:{self.version}:{self.author_did}:{self.lifecycle_state}"
|
|
49
|
-
self.signature = identity_key.sign_message(payload)
|
|
50
|
-
self.hash = hashlib.sha256(payload.encode()).hexdigest()
|
|
51
|
-
|
|
52
|
-
# --- 2. The Insight Hook (InsightExchange) ---
|
|
53
|
-
@dataclass
|
|
54
|
-
class InsightPacket:
|
|
55
|
-
"""Schema for Tradeable Information (The Data Unit)."""
|
|
56
|
-
topic: str
|
|
57
|
-
content_hash: str
|
|
58
|
-
value_score: float
|
|
59
|
-
source_did: str
|
|
60
|
-
timestamp: float
|
|
61
|
-
proof: Optional[str] = None # ZK Proof of validity (Future)
|
|
62
|
-
|
|
63
|
-
class InsightExchange:
|
|
64
|
-
"""Protocol for trading Insights between Daemons."""
|
|
65
|
-
def __init__(self, brain_path: Path):
|
|
66
|
-
self.ledger_path = brain_path / "network" / "insights.jsonl"
|
|
67
|
-
self.ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
-
|
|
69
|
-
def offer_insight(self, topic: str, content: str, value: float) -> InsightPacket:
|
|
70
|
-
"""Create an offer to sell info."""
|
|
71
|
-
packet = InsightPacket(
|
|
72
|
-
topic=topic,
|
|
73
|
-
content_hash=hashlib.sha256(content.encode()).hexdigest(),
|
|
74
|
-
value_score=value,
|
|
75
|
-
source_did="self", # In real impl, use IdentityKey
|
|
76
|
-
timestamp=time.time()
|
|
77
|
-
)
|
|
78
|
-
# Log offers
|
|
79
|
-
with open(self.ledger_path, "a") as f:
|
|
80
|
-
f.write(json.dumps(asdict(packet)) + "\n")
|
|
81
|
-
return packet
|
|
82
|
-
|
|
83
|
-
# --- 3. The Grid Hook (RemoteExecutionProtocol) ---
|
|
84
|
-
class RemoteExecutionProtocol:
|
|
85
|
-
"""Interface for offloading work to the Cloud (The Grid)."""
|
|
86
|
-
def __init__(self, brain_path: Path):
|
|
87
|
-
self.config_path = brain_path / "network" / "grid_config.json"
|
|
88
|
-
|
|
89
|
-
def dispatch_container(self, image: str, cmd: List[str], max_budget_usd: float) -> str:
|
|
90
|
-
"""
|
|
91
|
-
Stub: Dispatch a Docker container to a Grid Node (e.g. Cloud Run).
|
|
92
|
-
Returns: Execution ID.
|
|
93
|
-
"""
|
|
94
|
-
logger.info(f"🌩️ GRID DISPATCH: {image} (Budget: ${max_budget_usd})")
|
|
95
|
-
# In real impl, this calls Google Cloud Run API or a P2P compute market
|
|
96
|
-
return f"grid_exec_{uuid.uuid4()}"
|
|
97
|
-
|
|
98
|
-
# --- 4. The Host Hook (HostIntimacy) ---
|
|
99
|
-
class HostIntimacy:
|
|
100
|
-
"""
|
|
101
|
-
Permission-aware OS Access.
|
|
102
|
-
Managing the "Deep Binding" to the user's life (Files, Calendar, etc).
|
|
103
|
-
"""
|
|
104
|
-
def __init__(self):
|
|
105
|
-
self.permissions = {
|
|
106
|
-
"filesystem": "read-write",
|
|
107
|
-
"geolocation": "denied",
|
|
108
|
-
"clipboard": "ask",
|
|
109
|
-
"biometrics": "unavailable"
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
def check_permission(self, capability: str) -> bool:
|
|
113
|
-
"""Centralized permission check."""
|
|
114
|
-
return self.permissions.get(capability, "denied") in ["allowed", "read-write", "ask"]
|
|
115
|
-
|
|
116
|
-
# --- 5. The Identity Hook (IdentityKey) ---
|
|
117
|
-
class IdentityKey:
|
|
118
|
-
"""
|
|
119
|
-
Cryptographic Identity for the Sovereign Node (Ed25519).
|
|
120
|
-
The "Soul" of the machine.
|
|
121
|
-
"""
|
|
122
|
-
def __init__(self, brain_path: Path):
|
|
123
|
-
self.key_dir = brain_path / "identity"
|
|
124
|
-
self.key_dir.mkdir(parents=True, exist_ok=True)
|
|
125
|
-
self.private_key_path = self.key_dir / "node.pem"
|
|
126
|
-
self.public_key_path = self.key_dir / "node.pub"
|
|
127
|
-
|
|
128
|
-
self.did = ""
|
|
129
|
-
self._private_key = None
|
|
130
|
-
self._load_or_create_identity()
|
|
131
|
-
|
|
132
|
-
def _load_or_create_identity(self):
|
|
133
|
-
if not CRYPTO_AVAILABLE:
|
|
134
|
-
logger.warning("⚠️ 'cryptography' lib not found. Using UUID stub for Identity.")
|
|
135
|
-
self.did = f"did:nucleus:stub:{uuid.uuid4()}"
|
|
136
|
-
return
|
|
137
|
-
|
|
138
|
-
if self.private_key_path.exists():
|
|
139
|
-
try:
|
|
140
|
-
with open(self.private_key_path, "rb") as f:
|
|
141
|
-
self._private_key = serialization.load_pem_private_key(
|
|
142
|
-
f.read(), password=None
|
|
143
|
-
)
|
|
144
|
-
logger.info("🔑 Loaded existing IdentityKey.")
|
|
145
|
-
except Exception as e:
|
|
146
|
-
logger.error(f"❌ Failed to load IdentityKey: {e}. Generating new one.")
|
|
147
|
-
self._generate_new_key()
|
|
148
|
-
else:
|
|
149
|
-
self._generate_new_key()
|
|
150
|
-
|
|
151
|
-
# Derive Public Key and DID
|
|
152
|
-
if self._private_key:
|
|
153
|
-
public_key = self._private_key.public_key()
|
|
154
|
-
pub_bytes = public_key.public_bytes(
|
|
155
|
-
encoding=serialization.Encoding.Raw,
|
|
156
|
-
format=serialization.PublicFormat.Raw
|
|
157
|
-
)
|
|
158
|
-
# DID format: did:nucleus:<hex_pub_key>
|
|
159
|
-
self.did = f"did:nucleus:{pub_bytes.hex()}"
|
|
160
|
-
|
|
161
|
-
# Save public key for strict reference
|
|
162
|
-
with open(self.public_key_path, "wb") as f:
|
|
163
|
-
f.write(public_key.public_bytes(
|
|
164
|
-
encoding=serialization.Encoding.PEM,
|
|
165
|
-
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
166
|
-
))
|
|
167
|
-
|
|
168
|
-
def _generate_new_key(self):
|
|
169
|
-
logger.info("✨ Generating new Ed25519 IdentityKey...")
|
|
170
|
-
self._private_key = ed25519.Ed25519PrivateKey.generate()
|
|
171
|
-
|
|
172
|
-
# Save Private Key
|
|
173
|
-
pem = self._private_key.private_bytes(
|
|
174
|
-
encoding=serialization.Encoding.PEM,
|
|
175
|
-
format=serialization.PrivateFormat.PKCS8,
|
|
176
|
-
encryption_algorithm=serialization.NoEncryption()
|
|
177
|
-
)
|
|
178
|
-
with open(self.private_key_path, "wb") as f:
|
|
179
|
-
f.write(pem)
|
|
180
|
-
|
|
181
|
-
def sign_message(self, message: str) -> str:
|
|
182
|
-
"""Sign a payload using Ed25519."""
|
|
183
|
-
if not self._private_key:
|
|
184
|
-
return f"stub_sig_{uuid.uuid4()}"
|
|
185
|
-
|
|
186
|
-
points = message.encode("utf-8")
|
|
187
|
-
signature = self._private_key.sign(points)
|
|
188
|
-
return signature.hex()
|
|
189
|
-
|
|
190
|
-
def verify_signature(self, message: str, signature_hex: str, did: str) -> bool:
|
|
191
|
-
"""Verify a signature against a DID (Public Key)."""
|
|
192
|
-
if not CRYPTO_AVAILABLE or "stub" in did:
|
|
193
|
-
return True # Fallback for dev/stub modes
|
|
194
|
-
|
|
195
|
-
try:
|
|
196
|
-
# Extract Public Key from DID
|
|
197
|
-
if not did.startswith("did:nucleus:"):
|
|
198
|
-
return False
|
|
199
|
-
pub_key_hex = did.split(":")[-1]
|
|
200
|
-
pub_key_bytes = bytes.fromhex(pub_key_hex)
|
|
201
|
-
|
|
202
|
-
public_key = ed25519.Ed25519PublicKey.from_public_bytes(pub_key_bytes)
|
|
203
|
-
|
|
204
|
-
signature_bytes = bytes.fromhex(signature_hex)
|
|
205
|
-
public_key.verify(signature_bytes, message.encode("utf-8"))
|
|
206
|
-
return True
|
|
207
|
-
except Exception as e:
|
|
208
|
-
logger.warning(f"⚠️ Signature Verification Failed: {e}")
|
|
209
|
-
return False
|
|
210
|
-
|
|
211
|
-
# --- 6. The Pulse (AmbientTelemetry) ---
|
|
212
|
-
class AmbientTelemetry:
|
|
213
|
-
"""
|
|
214
|
-
Writes status to a "Pulse File" for the HUD/Status Bar to read.
|
|
215
|
-
Frequency: High (1s-5s).
|
|
216
|
-
|
|
217
|
-
Security (V9 Patch): Uses IdentityKey to sign heartbeats, preventing IPC hijacking.
|
|
218
|
-
"""
|
|
219
|
-
def __init__(self, brain_path: Path, identity: Optional['IdentityKey'] = None):
|
|
220
|
-
self.pulse_file = brain_path / "pulse.json"
|
|
221
|
-
self.identity = identity
|
|
222
|
-
|
|
223
|
-
def beat(self, status: str, active_tasks: int, cpu_usage: float = 0.0):
|
|
224
|
-
"""Write the heartbeat with a cryptographic pulse signal."""
|
|
225
|
-
timestamp = time.time()
|
|
226
|
-
data = {
|
|
227
|
-
"timestamp": timestamp,
|
|
228
|
-
"status": status, # idle, busy, error, sleeping
|
|
229
|
-
"tasks": active_tasks,
|
|
230
|
-
"cpu": cpu_usage,
|
|
231
|
-
"color": "green" if status == "idle" else "gold" if status == "busy" else "red"
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
# Add Trust Signal (Crypographic Pulse)
|
|
235
|
-
if self.identity:
|
|
236
|
-
payload = f"{timestamp}:{status}:{active_tasks}"
|
|
237
|
-
data["pulse_sig"] = self.identity.sign_message(payload)
|
|
238
|
-
data["did"] = self.identity.did
|
|
239
|
-
|
|
240
|
-
# Atomic write
|
|
241
|
-
try:
|
|
242
|
-
temp_file = self.pulse_file.with_suffix(".tmp")
|
|
243
|
-
temp_file.write_text(json.dumps(data))
|
|
244
|
-
temp_file.replace(self.pulse_file)
|
|
245
|
-
except Exception as e:
|
|
246
|
-
logger.warning(f"Failed to pulse: {e}")
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Gatekeeper: The Permission Broker.
|
|
3
|
-
Manages user consent ("CapabilityGrants") for sensitive operations.
|
|
4
|
-
|
|
5
|
-
Strategic Role:
|
|
6
|
-
- PERSISTENT: Remembers what the user allowed.
|
|
7
|
-
- GRANULAR: "Network: google.com" != "Network: *".
|
|
8
|
-
- INTERACTIVE: Bridges the gap between Agent Request and User Decision.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import logging
|
|
13
|
-
import hashlib
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Dict, Any
|
|
16
|
-
from pydantic import BaseModel
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger("GATEKEEPER")
|
|
19
|
-
|
|
20
|
-
ACCESS_DENIED = False
|
|
21
|
-
ACCESS_GRANTED = True
|
|
22
|
-
|
|
23
|
-
class GrantRequest(BaseModel):
|
|
24
|
-
agent_id: str
|
|
25
|
-
capability: str
|
|
26
|
-
params: Dict[str, Any] # e.g. {"domain": "google.com"} or {"path": "/tmp"}
|
|
27
|
-
|
|
28
|
-
def fingerprint(self) -> str:
|
|
29
|
-
"""Unique hash for this request signature"""
|
|
30
|
-
# Canonicalize params
|
|
31
|
-
import json
|
|
32
|
-
param_str = json.dumps(self.params, sort_keys=True)
|
|
33
|
-
raw = f"{self.agent_id}|{self.capability}|{param_str}"
|
|
34
|
-
return hashlib.sha256(raw.encode()).hexdigest()
|
|
35
|
-
|
|
36
|
-
class CapabilityGrant(BaseModel):
|
|
37
|
-
request_fingerprint: str
|
|
38
|
-
agent_id: str
|
|
39
|
-
capability: str
|
|
40
|
-
params: Dict[str, Any]
|
|
41
|
-
granted_at: str # ISO Timestamp
|
|
42
|
-
granted_by: str = "user"
|
|
43
|
-
|
|
44
|
-
class Gatekeeper:
|
|
45
|
-
def __init__(self, brain_path: Path):
|
|
46
|
-
self.brain_path = brain_path
|
|
47
|
-
self.ledger_path = brain_path / "ledger" / "permissions.json"
|
|
48
|
-
self._cache_grants: Dict[str, CapabilityGrant] = {}
|
|
49
|
-
self._load_ledger()
|
|
50
|
-
|
|
51
|
-
def _load_ledger(self):
|
|
52
|
-
self.ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
-
if self.ledger_path.exists():
|
|
54
|
-
try:
|
|
55
|
-
data = json.loads(self.ledger_path.read_text())
|
|
56
|
-
for item in data:
|
|
57
|
-
grant = CapabilityGrant(**item)
|
|
58
|
-
self._cache_grants[grant.request_fingerprint] = grant
|
|
59
|
-
except Exception as e:
|
|
60
|
-
logger.error(f"Failed to load permission ledger: {e}")
|
|
61
|
-
|
|
62
|
-
def _save_ledger(self):
|
|
63
|
-
data = [g.model_dump() for g in self._cache_grants.values()]
|
|
64
|
-
self.ledger_path.write_text(json.dumps(data, indent=2))
|
|
65
|
-
|
|
66
|
-
def check_permission(self, request: GrantRequest) -> bool:
|
|
67
|
-
"""
|
|
68
|
-
Check if a grant exists for this specific request.
|
|
69
|
-
"""
|
|
70
|
-
fp = request.fingerprint()
|
|
71
|
-
if fp in self._cache_grants:
|
|
72
|
-
return ACCESS_GRANTED
|
|
73
|
-
|
|
74
|
-
# TODO: Implement wildcard support?
|
|
75
|
-
# For Phase 57, we do strict equality.
|
|
76
|
-
|
|
77
|
-
return ACCESS_DENIED
|
|
78
|
-
|
|
79
|
-
def grant_permission(self, request: GrantRequest):
|
|
80
|
-
"""
|
|
81
|
-
Persist a new grant.
|
|
82
|
-
"""
|
|
83
|
-
from datetime import datetime
|
|
84
|
-
fp = request.fingerprint()
|
|
85
|
-
|
|
86
|
-
grant = CapabilityGrant(
|
|
87
|
-
request_fingerprint=fp,
|
|
88
|
-
agent_id=request.agent_id,
|
|
89
|
-
capability=request.capability,
|
|
90
|
-
params=request.params,
|
|
91
|
-
granted_at=datetime.now().isoformat()
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
self._cache_grants[fp] = grant
|
|
95
|
-
self._save_ledger()
|
|
96
|
-
logger.info(f"Granted permission: {request.agent_id} -> {request.capability} {request.params}")
|
|
97
|
-
|
|
98
|
-
def revoke_permission(self, request: GrantRequest):
|
|
99
|
-
"""
|
|
100
|
-
Revoke a grant.
|
|
101
|
-
"""
|
|
102
|
-
fp = request.fingerprint()
|
|
103
|
-
if fp in self._cache_grants:
|
|
104
|
-
del self._cache_grants[fp]
|
|
105
|
-
self._save_ledger()
|
|
106
|
-
logger.info(f"Revoked permission: {request.agent_id}")
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import hashlib
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
from datetime import datetime, timezone
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import NamedTuple, Optional, Dict
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
from cryptography.hazmat.primitives import serialization
|
|
10
|
-
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
11
|
-
HAS_CRYPTO = True
|
|
12
|
-
except ImportError:
|
|
13
|
-
HAS_CRYPTO = False
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger("KEY_MANAGER")
|
|
16
|
-
|
|
17
|
-
class KeyPair(NamedTuple):
|
|
18
|
-
key_id: str
|
|
19
|
-
private_key_pem: str
|
|
20
|
-
public_key_pem: str
|
|
21
|
-
|
|
22
|
-
class KeyManager:
|
|
23
|
-
"""
|
|
24
|
-
Manages Ed25519 Key Generation, Verification, and Persistence for Nucleus.
|
|
25
|
-
"""
|
|
26
|
-
def __init__(self, brain_path: Optional[Path] = None):
|
|
27
|
-
"""
|
|
28
|
-
Args:
|
|
29
|
-
brain_path: Optional root path for key storage. If None, runs in stateless mode.
|
|
30
|
-
"""
|
|
31
|
-
if not HAS_CRYPTO:
|
|
32
|
-
raise ImportError("cryptography module is required. Install with: pip install cryptography")
|
|
33
|
-
|
|
34
|
-
self.brain_path = brain_path
|
|
35
|
-
if self.brain_path:
|
|
36
|
-
self.keystore_path = self.brain_path / "ledger" / "keystore.json"
|
|
37
|
-
self.keystore_path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
-
|
|
39
|
-
def _load_keystore(self) -> Dict[str, Dict[str, str]]:
|
|
40
|
-
if not self.brain_path or not self.keystore_path.exists():
|
|
41
|
-
return {}
|
|
42
|
-
try:
|
|
43
|
-
return json.loads(self.keystore_path.read_text())
|
|
44
|
-
except Exception:
|
|
45
|
-
return {}
|
|
46
|
-
|
|
47
|
-
def _save_keystore(self, store: Dict):
|
|
48
|
-
if self.brain_path:
|
|
49
|
-
# We don't have locking here yet, assume single user or add locking later
|
|
50
|
-
# Ideally use update_brain_file or lock
|
|
51
|
-
self.keystore_path.write_text(json.dumps(store, indent=2))
|
|
52
|
-
|
|
53
|
-
def generate_key(self, alias: Optional[str] = None) -> str:
|
|
54
|
-
"""
|
|
55
|
-
Generates, Persists, and returns Key ID.
|
|
56
|
-
Args:
|
|
57
|
-
alias: Optional human readable alias (e.g. 'publisher_main')
|
|
58
|
-
"""
|
|
59
|
-
kp = self.generate_keypair()
|
|
60
|
-
|
|
61
|
-
if self.brain_path:
|
|
62
|
-
store = self._load_keystore()
|
|
63
|
-
store[kp.key_id] = {
|
|
64
|
-
"private_key_pem": kp.private_key_pem,
|
|
65
|
-
"public_key_pem": kp.public_key_pem,
|
|
66
|
-
"alias": alias,
|
|
67
|
-
"created_at": datetime.now(timezone.utc).isoformat()
|
|
68
|
-
}
|
|
69
|
-
if alias:
|
|
70
|
-
# Also index by alias for convenience?
|
|
71
|
-
# Or just search. Let's just store by ID for now.
|
|
72
|
-
pass
|
|
73
|
-
self._save_keystore(store)
|
|
74
|
-
|
|
75
|
-
return kp.key_id
|
|
76
|
-
|
|
77
|
-
def get_key_pair(self, key_id: str) -> Optional[KeyPair]:
|
|
78
|
-
"""Retrieve key pair from disk by ID."""
|
|
79
|
-
if not self.brain_path:
|
|
80
|
-
return None
|
|
81
|
-
|
|
82
|
-
store = self._load_keystore()
|
|
83
|
-
if key_id in store:
|
|
84
|
-
data = store[key_id]
|
|
85
|
-
return KeyPair(
|
|
86
|
-
key_id=key_id,
|
|
87
|
-
private_key_pem=data["private_key_pem"],
|
|
88
|
-
public_key_pem=data["public_key_pem"]
|
|
89
|
-
)
|
|
90
|
-
return None
|
|
91
|
-
|
|
92
|
-
def generate_keypair(self) -> KeyPair:
|
|
93
|
-
"""Generates a new Ed25519 keypair and returns PEM strings (Stateless)."""
|
|
94
|
-
private_key = ed25519.Ed25519PrivateKey.generate()
|
|
95
|
-
public_key = private_key.public_key()
|
|
96
|
-
|
|
97
|
-
# Serialize Private Key
|
|
98
|
-
private_pem = private_key.private_bytes(
|
|
99
|
-
encoding=serialization.Encoding.PEM,
|
|
100
|
-
format=serialization.PrivateFormat.PKCS8,
|
|
101
|
-
encryption_algorithm=serialization.NoEncryption()
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
# Serialize Public Key
|
|
105
|
-
public_pem = public_key.public_bytes(
|
|
106
|
-
encoding=serialization.Encoding.PEM,
|
|
107
|
-
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
# Generate Key ID (Fingerprint of Public Key)
|
|
111
|
-
public_der = public_key.public_bytes(
|
|
112
|
-
encoding=serialization.Encoding.DER,
|
|
113
|
-
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
114
|
-
)
|
|
115
|
-
key_id = hashlib.sha256(public_der).hexdigest()
|
|
116
|
-
|
|
117
|
-
return KeyPair(
|
|
118
|
-
key_id=key_id,
|
|
119
|
-
private_key_pem=private_pem.decode('utf-8'),
|
|
120
|
-
public_key_pem=public_pem.decode('utf-8')
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
def sign(self, private_key_pem: str, data: bytes) -> bytes:
|
|
124
|
-
"""Signs data with private key PEM. Returns raw bytes signature."""
|
|
125
|
-
private_key = serialization.load_pem_private_key(
|
|
126
|
-
private_key_pem.encode('utf-8'),
|
|
127
|
-
password=None
|
|
128
|
-
)
|
|
129
|
-
if not isinstance(private_key, ed25519.Ed25519PrivateKey):
|
|
130
|
-
raise ValueError("Invalid Key Type. Expected Ed25519PrivateKey.")
|
|
131
|
-
|
|
132
|
-
return private_key.sign(data)
|
|
133
|
-
|
|
134
|
-
def verify(self, public_key_pem: str, signature: bytes, data: bytes) -> bool:
|
|
135
|
-
"""Verifies signature with public key PEM. Returns True if valid."""
|
|
136
|
-
try:
|
|
137
|
-
public_key = serialization.load_pem_public_key(
|
|
138
|
-
public_key_pem.encode('utf-8')
|
|
139
|
-
)
|
|
140
|
-
if not isinstance(public_key, ed25519.Ed25519PublicKey):
|
|
141
|
-
raise ValueError("Invalid Key Type. Expected Ed25519PublicKey.")
|
|
142
|
-
|
|
143
|
-
public_key.verify(signature, data)
|
|
144
|
-
return True
|
|
145
|
-
except Exception as e:
|
|
146
|
-
logger.warning(f"Verification Failed: {e}")
|
|
147
|
-
return False
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from pydantic import BaseModel, Field, validator, ValidationError
|
|
3
|
-
from typing import List, Optional, Literal, Dict, Any
|
|
4
|
-
|
|
5
|
-
class CapabilityScope(str, Enum):
|
|
6
|
-
NETWORK = "network"
|
|
7
|
-
FILESYSTEM = "filesystem"
|
|
8
|
-
MEMORY = "memory"
|
|
9
|
-
SHELL = "shell"
|
|
10
|
-
STRATEGY = "strategy"
|
|
11
|
-
BROWSER = "browser"
|
|
12
|
-
|
|
13
|
-
class Capability(BaseModel):
|
|
14
|
-
scope: CapabilityScope
|
|
15
|
-
reason: str = Field(..., description="Why this capability is needed")
|
|
16
|
-
|
|
17
|
-
# Scope-specific fields
|
|
18
|
-
domains: Optional[List[str]] = Field(None, description="For network scope: Whitelisted domains")
|
|
19
|
-
paths: Optional[List[str]] = Field(None, description="For filesystem/strategy scope: Allowed paths")
|
|
20
|
-
mode: Optional[Literal["read", "write", "readwrite", "read_write"]] = Field(None, description="For filesystem/strategy scope: Access mode")
|
|
21
|
-
|
|
22
|
-
@validator("domains")
|
|
23
|
-
def validate_network_domains(cls, v, values):
|
|
24
|
-
if values.get("scope") == CapabilityScope.NETWORK and not v:
|
|
25
|
-
raise ValueError("Network capability must specify 'domains' whitelist.")
|
|
26
|
-
return v
|
|
27
|
-
|
|
28
|
-
@validator("paths")
|
|
29
|
-
def validate_fs_paths(cls, v, values):
|
|
30
|
-
scope = values.get("scope")
|
|
31
|
-
if scope in [CapabilityScope.FILESYSTEM, CapabilityScope.STRATEGY] and not v:
|
|
32
|
-
raise ValueError(f"{scope} capability must specify 'paths' whitelist.")
|
|
33
|
-
return v
|
|
34
|
-
|
|
35
|
-
class AgentIdentity(BaseModel):
|
|
36
|
-
id: str = Field(..., pattern=r"^[a-z0-9]+(\.[a-z0-9]+)+$", description="Reverse DNS ID (e.g. nucleus.core.ops)")
|
|
37
|
-
name: str
|
|
38
|
-
version: str = Field(..., pattern=r"^\d+\.\d+\.\d+$", description="Semantic Version")
|
|
39
|
-
description: str
|
|
40
|
-
author: str
|
|
41
|
-
license: str
|
|
42
|
-
|
|
43
|
-
class LifecyclePolicy(BaseModel):
|
|
44
|
-
persistence: Literal["session", "persistent", "immortal"] = "session"
|
|
45
|
-
cleanup: Literal["strict", "lazy", "none"] = "strict"
|
|
46
|
-
|
|
47
|
-
class AgentManifest(BaseModel):
|
|
48
|
-
manifest_version: str = "1.0.0"
|
|
49
|
-
agent: AgentIdentity
|
|
50
|
-
capabilities: List[Capability] = Field(default_factory=list)
|
|
51
|
-
lifecycle: LifecyclePolicy = Field(default_factory=LifecyclePolicy)
|
|
52
|
-
|
|
53
|
-
class ManifestValidator:
|
|
54
|
-
"""
|
|
55
|
-
Validates Agent Manifests against the strict zero-trust schema.
|
|
56
|
-
"""
|
|
57
|
-
@staticmethod
|
|
58
|
-
def validate(manifest_data: Dict[str, Any]) -> AgentManifest:
|
|
59
|
-
try:
|
|
60
|
-
return AgentManifest(**manifest_data)
|
|
61
|
-
except ValidationError as e:
|
|
62
|
-
raise ValueError(f"Manifest Validation Failed: {e}")
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from pydantic import BaseModel, Field
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
class TrustLevel(str, Enum):
|
|
6
|
-
VERIFIED = "verified" # Signed by Nucleus Root or known partner
|
|
7
|
-
COMMUNITY = "community" # Signed by unknown key, but structure valid
|
|
8
|
-
UNKNOWN = "unknown" # Unsigned or invalid
|
|
9
|
-
MALICIOUS = "malicious" # Specific deny-list
|
|
10
|
-
|
|
11
|
-
class TrustProfile(BaseModel):
|
|
12
|
-
"""
|
|
13
|
-
Represents the Identity of an Agent Publisher.
|
|
14
|
-
This corresponds to the 'signer' of the .nuke artifact.
|
|
15
|
-
"""
|
|
16
|
-
publisher_id: str = Field(..., description="Unique Key ID (Fingerprint)")
|
|
17
|
-
trust_level: TrustLevel = Field(default=TrustLevel.UNKNOWN, description="Current trust status")
|
|
18
|
-
label: str = Field(..., description="Human readable name (e.g., 'Antigravity')")
|
|
19
|
-
public_key: str = Field(..., description="PEM encoded Public Key")
|
|
20
|
-
verification_url: Optional[str] = Field(None, description="URL to verify identity (e.g. GitHub Gist)")
|
|
21
|
-
|
|
22
|
-
def to_json(self) -> str:
|
|
23
|
-
return self.model_dump_json(indent=2)
|
|
24
|
-
|
|
25
|
-
@classmethod
|
|
26
|
-
def from_json(cls, json_str: str) -> "TrustProfile":
|
|
27
|
-
return cls.model_validate_json(json_str)
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Inspector: The Lens.
|
|
3
|
-
Provides deep introspection into Agent Manifests for security review.
|
|
4
|
-
|
|
5
|
-
Role:
|
|
6
|
-
- Renders human-readable reports of what an agent can do.
|
|
7
|
-
- Highlights high-risk capabilities (Network, Shell, FS).
|
|
8
|
-
- Verifies Trust Chains (Future scope).
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from .identity.manifest import AgentManifest, CapabilityScope
|
|
12
|
-
|
|
13
|
-
class ManifestViewer:
|
|
14
|
-
@staticmethod
|
|
15
|
-
def render_report(manifest: AgentManifest) -> str:
|
|
16
|
-
"""
|
|
17
|
-
Generates a formatted text report of the agent's identity and capabilities.
|
|
18
|
-
"""
|
|
19
|
-
agent = manifest.agent
|
|
20
|
-
lines = []
|
|
21
|
-
|
|
22
|
-
# Header
|
|
23
|
-
lines.append("╔═══════════════════════════════════════════════════════════╗")
|
|
24
|
-
lines.append(f"║ IDENTITY: {agent.name:<47} ║")
|
|
25
|
-
lines.append(f"║ ID: {agent.id:<47} ║")
|
|
26
|
-
lines.append(f"║ Version: {agent.version:<47} ║")
|
|
27
|
-
lines.append(f"║ License: {agent.license:<47} ║")
|
|
28
|
-
lines.append("╟───────────────────────────────────────────────────────────╢")
|
|
29
|
-
|
|
30
|
-
# Description
|
|
31
|
-
desc_preview = (agent.description[:50] + '..') if len(agent.description) > 50 else agent.description
|
|
32
|
-
lines.append(f"║ {desc_preview:<57} ║")
|
|
33
|
-
lines.append("╠═══════════════════════════════════════════════════════════╣")
|
|
34
|
-
|
|
35
|
-
# Capabilities
|
|
36
|
-
if not manifest.capabilities:
|
|
37
|
-
lines.append("║ ✅ No Capabilities Requested (Safe) ║")
|
|
38
|
-
else:
|
|
39
|
-
lines.append("║ ⚠️ CAPABILITIES REQUESTED ║")
|
|
40
|
-
lines.append("╟───────────────────────────────────────────────────────────╢")
|
|
41
|
-
|
|
42
|
-
for cap in manifest.capabilities:
|
|
43
|
-
scope = cap.scope.value.upper()
|
|
44
|
-
icon = ManifestViewer._get_icon(cap.scope)
|
|
45
|
-
|
|
46
|
-
lines.append(f"║ {icon} {scope:<53} ║")
|
|
47
|
-
lines.append(f"║ Reason: {cap.reason[:44]:<44} ║")
|
|
48
|
-
|
|
49
|
-
# Details
|
|
50
|
-
if cap.scope == CapabilityScope.NETWORK and cap.domains:
|
|
51
|
-
lines.append(f"║ Domains: {', '.join(cap.domains)[:43]:<43} ║")
|
|
52
|
-
|
|
53
|
-
if cap.scope == CapabilityScope.FILESYSTEM and cap.paths:
|
|
54
|
-
mode = getattr(cap, 'mode', 'read')
|
|
55
|
-
lines.append(f"║ [{mode.upper()}] Paths: {', '.join(cap.paths)[:38]:<38} ║")
|
|
56
|
-
|
|
57
|
-
lines.append("╟───────────────────────────────────────────────────────────╢")
|
|
58
|
-
|
|
59
|
-
lines.append("╚═══════════════════════════════════════════════════════════╝")
|
|
60
|
-
return "\n".join(lines)
|
|
61
|
-
|
|
62
|
-
@staticmethod
|
|
63
|
-
def _get_icon(scope: CapabilityScope) -> str:
|
|
64
|
-
if scope == CapabilityScope.NETWORK:
|
|
65
|
-
return "⚠️ "
|
|
66
|
-
if scope == CapabilityScope.SHELL:
|
|
67
|
-
return "🚨"
|
|
68
|
-
if scope == CapabilityScope.FILESYSTEM:
|
|
69
|
-
return "📁"
|
|
70
|
-
if scope == CapabilityScope.MEMORY:
|
|
71
|
-
return "🧠"
|
|
72
|
-
if scope == CapabilityScope.BROWSER:
|
|
73
|
-
return "🌐"
|
|
74
|
-
return "🔧"
|