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,210 +0,0 @@
1
- """
2
- Recursive Mounter Prototype (AG-021).
3
- Enables Nucleus to act as a Host-Runtime for external MCP servers.
4
- Supported Transports: stdio (MVP).
5
- """
6
-
7
- import asyncio
8
- import json
9
- import logging
10
- import uuid
11
- from pathlib import Path
12
- from typing import Dict, Any, List, Optional
13
-
14
- logger = logging.getLogger("MOUNTER")
15
-
16
-
17
- class MountedServer:
18
- def __init__(self, name: str, command: str, args: List[str]):
19
- self.name = name
20
- self.command = command
21
- self.args = args
22
- self.process: Optional[asyncio.subprocess.Process] = None
23
- self.id = f"mnt-{uuid.uuid4().hex[:6]}"
24
- self.mounted_at = asyncio.get_event_loop().time()
25
- self.tools: List[Dict] = []
26
-
27
- async def start(self):
28
- """Starts the external MCP server via stdio."""
29
- logger.info(f"🚀 Starting Mounted Server: {self.name} ({self.command} {' '.join(self.args)})")
30
- self.process = await asyncio.create_subprocess_exec(
31
- self.command, *self.args,
32
- stdin=asyncio.subprocess.PIPE,
33
- stdout=asyncio.subprocess.PIPE,
34
- stderr=asyncio.subprocess.PIPE
35
- )
36
- # In a real implementation, we would now negotiate list_tools
37
- # For the prototype, we assume it's running.
38
- return True
39
-
40
- async def stop(self):
41
- if self.process:
42
- self.process.terminate()
43
- await self.process.wait()
44
- logger.info(f"🛑 Stopped Mounted Server: {self.name}")
45
-
46
- async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict:
47
- """Proxies a tool call to the mounted server via MCP (stdio)."""
48
- if not self.process:
49
- raise RuntimeError(f"Server {self.name} is not running")
50
-
51
- request = {
52
- "jsonrpc": "2.0",
53
- "method": "tools/call",
54
- "params": {
55
- "name": tool_name,
56
- "arguments": arguments
57
- },
58
- "id": str(uuid.uuid4())
59
- }
60
-
61
- # Send request
62
- self.process.stdin.write((json.dumps(request) + "\n").encode())
63
- await self.process.stdin.drain()
64
-
65
- # Read response (one line)
66
- line = await self.process.stdout.readline()
67
- if not line:
68
- return {"success": False, "error": "No response from mounted server"}
69
-
70
- return json.loads(line.decode())
71
-
72
- async def list_tools(self) -> List[Dict]:
73
- """Discover tools from the mounted server via MCP protocol."""
74
- if not self.process:
75
- return []
76
-
77
- request = {
78
- "jsonrpc": "2.0",
79
- "method": "tools/list",
80
- "params": {},
81
- "id": str(uuid.uuid4())
82
- }
83
-
84
- try:
85
- self.process.stdin.write((json.dumps(request) + "\n").encode())
86
- await self.process.stdin.drain()
87
-
88
- # Read response with timeout
89
- line = await asyncio.wait_for(
90
- self.process.stdout.readline(),
91
- timeout=5.0
92
- )
93
- if not line:
94
- return []
95
-
96
- response = json.loads(line.decode())
97
- if "result" in response and "tools" in response["result"]:
98
- self.tools = response["result"]["tools"]
99
- return self.tools
100
- return []
101
- except asyncio.TimeoutError:
102
- logger.warning(f"Timeout discovering tools from {self.name}")
103
- return []
104
- except Exception as e:
105
- logger.error(f"Error discovering tools from {self.name}: {e}")
106
- return []
107
-
108
-
109
-
110
- class RecursiveMounter:
111
- def __init__(self, brain_path: Path):
112
- self.brain_path = brain_path
113
- self.mounted_servers: Dict[str, MountedServer] = {}
114
- self.mounts_file = brain_path / "ledger" / "mounts.json"
115
- self.mounts_file.parent.mkdir(parents=True, exist_ok=True)
116
- self._load_mounts()
117
-
118
- def _load_mounts(self):
119
- """Load persistent mounts from disk."""
120
- if not self.mounts_file.exists():
121
- return
122
-
123
- try:
124
- data = json.loads(self.mounts_file.read_text())
125
- for mount_data in data:
126
- # We don't auto-start on load in this MVP, but we restore the config
127
- # In a real OS, we would have a startup supervisor.
128
- # For now, we just track them.
129
- server = MountedServer(
130
- name=mount_data["name"],
131
- command=mount_data["command"],
132
- args=mount_data["args"]
133
- )
134
- server.id = mount_data["id"]
135
- self.mounted_servers[server.id] = server
136
- except Exception as e:
137
- logger.error(f"Failed to load mounts: {e}")
138
-
139
- def _save_mounts(self):
140
- """Save persistent mounts to disk."""
141
- data = [
142
- {
143
- "id": s.id,
144
- "name": s.name,
145
- "command": s.command,
146
- "args": s.args
147
- }
148
- for s in self.mounted_servers.values()
149
- ]
150
- self.mounts_file.write_text(json.dumps(data, indent=2))
151
-
152
- async def mount(self, name: str, command: str, args: List[str]) -> str:
153
- """Mounts an external MCP server."""
154
- # Check for existing name
155
- if not name or len(name.strip()) < 2:
156
- return "Error: Server name must be at least 2 characters"
157
-
158
- # V9.1 Security: Recursive Self-Mount Detection
159
- # Check if the command or args try to launch the nucleus server itself
160
- forbidden_patterns = ["mcp_server_nucleus", "mcp-server-nucleus"]
161
- cmd_str = f"{command} {' '.join(args)}".lower()
162
- for pattern in forbidden_patterns:
163
- if pattern in cmd_str:
164
- import sys
165
- print("[NUCLEUS] SECURITY VIOLATION: Recursive mount detected", file=sys.stderr)
166
- return f"Error: Recursive mounting of {pattern} is forbidden for stability."
167
-
168
- for s in self.mounted_servers.values():
169
- if s.name == name:
170
- return f"Server {name} is already mounted as {s.id}"
171
-
172
- server = MountedServer(name, command, args)
173
- try:
174
- await server.start()
175
- self.mounted_servers[server.id] = server
176
- self._save_mounts()
177
- return f"Successfully mounted {name} as {server.id}"
178
- except Exception as e:
179
- return f"Failed to mount {name}: {e}"
180
-
181
- async def unmount(self, server_id: str) -> str:
182
- """Unmounts and stops a server."""
183
- if server_id not in self.mounted_servers:
184
- return f"Server {server_id} not found"
185
-
186
- server = self.mounted_servers[server_id]
187
- await server.stop()
188
- del self.mounted_servers[server_id]
189
- self._save_mounts()
190
- return f"Unmounted {server.name} ({server_id})"
191
-
192
- def list_mounted(self) -> List[Dict]:
193
- return [
194
- {
195
- "id": s.id,
196
- "name": s.name,
197
- "command": s.command,
198
- "status": "Running" if s.process and s.process.returncode is None else "Stopped"
199
- }
200
- for s in self.mounted_servers.values()
201
- ]
202
-
203
- # Global singleton for prototype
204
- _mounter: Optional[RecursiveMounter] = None
205
-
206
- def get_mounter(brain_path: Path) -> RecursiveMounter:
207
- global _mounter
208
- if _mounter is None:
209
- _mounter = RecursiveMounter(brain_path)
210
- return _mounter
@@ -1,250 +0,0 @@
1
-
2
- import json
3
- import os
4
- import logging
5
- from typing import Dict, List, Any
6
- from pathlib import Path
7
-
8
- # Try importing mcp, handle failure gracefully for the "no dependency" case
9
- try:
10
- from mcp import ClientSession, StdioServerParameters
11
- from mcp.client.stdio import stdio_client
12
- from mcp.client.sse import sse_client
13
- MCP_AVAILABLE = True
14
- except ImportError:
15
- MCP_AVAILABLE = False
16
- # Mock classes for type hinting if missing
17
- ClientSession = Any
18
- StdioServerParameters = Any
19
-
20
- logger = logging.getLogger("nucleus_mounter")
21
-
22
- class Mounter:
23
- """
24
- Manages connections to external MCP servers (Recursive Mounting).
25
- Supports Stdio and SSE transports.
26
- """
27
- def __init__(self, brain_path: Path):
28
- self.brain_path = brain_path
29
- self.mounts_file = brain_path / "mounts.json"
30
-
31
- # In-memory session store: mount_id -> ClientSession
32
- self.sessions: Dict[str, ClientSession] = {}
33
- # Keep track of exit stack context managers to close them later
34
- self.exit_stacks: Dict[str, Any] = {}
35
-
36
- self.mount_configs: Dict[str, Dict] = {}
37
- self._load_mounts()
38
-
39
- def _load_mounts(self):
40
- """Load persisted mount configurations."""
41
- if self.mounts_file.exists():
42
- try:
43
- with open(self.mounts_file, "r") as f:
44
- self.mount_configs = json.load(f)
45
- except Exception as e:
46
- logger.error(f"Failed to load mounts: {e}")
47
- async def restore_mounts(self):
48
- """Re-establish connections for all persisted mounts."""
49
- print(f"🔄 Restoring {len(self.mount_configs)} mounts...")
50
- for mount_id, config in self.mount_configs.items():
51
- try:
52
- if config.get("transport") == "stdio":
53
- await self.mount_server(
54
- mount_id=mount_id,
55
- transport="stdio",
56
- command=config.get("command"),
57
- args=config.get("args"),
58
- env=config.get("env")
59
- )
60
- print(f" ✅ Re-mounted: {mount_id}")
61
- elif config.get("transport") == "sse":
62
- await self.mount_server(
63
- mount_id=mount_id,
64
- transport="sse",
65
- url=config.get("url")
66
- )
67
- print(f" ✅ Re-mounted: {mount_id}")
68
- except Exception as e:
69
- logger.error(f"Failed to restore mount {mount_id}: {e}")
70
- print(f" ❌ Failed to restore {mount_id}: {e}")
71
- def _save_mounts(self):
72
- """Persist mount configurations."""
73
- try:
74
- with open(self.mounts_file, "w") as f:
75
- json.dump(self.mount_configs, f, indent=2)
76
- except Exception as e:
77
- logger.error(f"Failed to save mounts: {e}")
78
-
79
- async def mount_server(self,
80
- mount_id: str,
81
- transport: str,
82
- command: str = None,
83
- args: List[str] = None,
84
- env: Dict[str, str] = None,
85
- url: str = None) -> Dict[str, Any]:
86
- """
87
- Mount an MCP server.
88
-
89
- Args:
90
- mount_id: Unique identifier for this mount.
91
- transport: "stdio" or "sse".
92
- command: (stdio) Executable command.
93
- args: (stdio) List of arguments.
94
- env: (stdio) Environment variables.
95
- url: (sse) Server URL.
96
- """
97
- if not MCP_AVAILABLE:
98
- raise ImportError("The 'mcp' package is not installed. Please install it to use mounting features.")
99
-
100
- if mount_id in self.sessions:
101
- raise ValueError(f"Mount ID '{mount_id}' is already active.")
102
-
103
- if transport == "stdio":
104
- if not command:
105
- raise ValueError("Command is required for stdio transport")
106
-
107
- server_params = StdioServerParameters(
108
- command=command,
109
- args=args or [],
110
- env={**os.environ, **(env or {})}
111
- )
112
-
113
- # We need to maintain the context manager lifecycle
114
- # This is a bit tricky in an async methods that keeps the session open.
115
- # We'll use an AsyncExitStack stored in class state.
116
- from contextlib import AsyncExitStack
117
-
118
- stack = AsyncExitStack()
119
- stdio_transport = await stack.enter_async_context(stdio_client(server_params))
120
- self.exit_stacks[mount_id] = stack
121
-
122
- read, write = stdio_transport
123
- session = await stack.enter_async_context(ClientSession(read, write))
124
- await session.initialize()
125
-
126
- self.sessions[mount_id] = session
127
-
128
- # Persist config
129
- self.mount_configs[mount_id] = {
130
- "transport": "stdio",
131
- "command": command,
132
- "args": args,
133
- "env": env,
134
- "status": "connected"
135
- }
136
- self._save_mounts()
137
-
138
- return {"mount_id": mount_id, "status": "connected", "tools": len((await session.list_tools()).tools)}
139
-
140
- elif transport == "sse":
141
- if not url:
142
- raise ValueError("URL is required for SSE transport")
143
-
144
- from contextlib import AsyncExitStack
145
- stack = AsyncExitStack()
146
- sse_transport = await stack.enter_async_context(sse_client(url))
147
- self.exit_stacks[mount_id] = stack
148
-
149
- read, write = sse_transport
150
- session = await stack.enter_async_context(ClientSession(read, write))
151
- await session.initialize()
152
-
153
- self.sessions[mount_id] = session
154
-
155
- self.mount_configs[mount_id] = {
156
- "transport": "sse",
157
- "url": url,
158
- "status": "connected"
159
- }
160
- self._save_mounts()
161
-
162
- return {"mount_id": mount_id, "status": "connected", "tools": len((await session.list_tools()).tools)}
163
-
164
- else:
165
- raise ValueError(f"Unknown transport: {transport}")
166
-
167
- async def unmount_server(self, mount_id: str):
168
- """Unmount a server and close connection."""
169
- if mount_id in self.exit_stacks:
170
- await self.exit_stacks[mount_id].aclose()
171
- del self.exit_stacks[mount_id]
172
-
173
- if mount_id in self.sessions:
174
- del self.sessions[mount_id]
175
-
176
- if mount_id in self.mount_configs:
177
- del self.mount_configs[mount_id]
178
- self._save_mounts()
179
-
180
- async def list_mounts(self) -> List[Dict]:
181
- """List active mounts."""
182
- return [
183
- {"id": k, **v} for k, v in self.mount_configs.items()
184
- ]
185
-
186
- async def list_tools(self) -> List[Dict]:
187
- """
188
- Aggregate tools from all mounted servers.
189
- Tools are namespaced as 'mount_id:tool_name'.
190
- """
191
- aggregated_tools = []
192
-
193
- for mount_id, session in self.sessions.items():
194
- try:
195
- result = await session.list_tools()
196
- for tool in result.tools:
197
- # Create a namespaced copy of the tool definition
198
- tool_dict = tool.model_dump() if hasattr(tool, "model_dump") else tool.__dict__
199
- tool_dict["name"] = f"{mount_id}:{tool.name}"
200
- tool_dict["description"] = f"[{mount_id}] {tool_dict.get('description', '')}"
201
- aggregated_tools.append(tool_dict)
202
- except Exception as e:
203
- logger.error(f"Error listing tools for {mount_id}: {e}")
204
-
205
- return aggregated_tools
206
-
207
- async def call_tool(self, name: str, args: Dict) -> Any:
208
- """
209
- Call a namespaced tool.
210
- Name format: 'mount_id:tool_name'
211
- """
212
- if ":" not in name:
213
- raise ValueError("Invalid namespaced tool name. Format must be 'mount_id:tool_name'")
214
-
215
- mount_id, tool_name = name.split(":", 1)
216
-
217
- if mount_id not in self.sessions:
218
- raise ValueError(f"Mount ID '{mount_id}' not found")
219
-
220
- session = self.sessions[mount_id]
221
- result = await session.call_tool(tool_name, arguments=args)
222
- return result
223
-
224
- async def traverse_and_mount(self, root_mount_id: str) -> Dict:
225
- """
226
- Recursive discovery logic.
227
- Inspects tools of a mounted server to find other 'mount_server' compatible capabilities.
228
- This is a heuristic implementation for the demo.
229
- """
230
- if root_mount_id not in self.sessions:
231
- raise ValueError(f"Root mount '{root_mount_id}' not found")
232
-
233
- session = self.sessions[root_mount_id]
234
- tools = await session.list_tools()
235
-
236
- found_servers = []
237
-
238
- # Heuristic: Look for tools that return 'mcp_server' or have 'mount' in description
239
- # In a real scenario, this would use a standardized 'list_servers' tool or resource.
240
- # For the demo, we'll just report what we see.
241
-
242
- for tool in tools.tools:
243
- if "server" in tool.name.lower() or "mount" in tool.name.lower():
244
- found_servers.append(tool.name)
245
-
246
- return {
247
- "root": root_mount_id,
248
- "potential_sub_servers": found_servers,
249
- "status": "traversal_complete"
250
- }
@@ -1,147 +0,0 @@
1
- """
2
- The .nuke Protocol (Nucleus Universal Knowledge Exchange).
3
- Phase 57 Implementation.
4
-
5
- Format: ZIP Archive
6
- Structure:
7
- /manifest.json (AgentManifest Schema)
8
- /signature.sig (Ed25519 Signature of manifest.json)
9
- /tools/ (Python Code)
10
- """
11
-
12
- import json
13
- import logging
14
- import zipfile
15
- import shutil
16
- import tempfile
17
- from pathlib import Path
18
- from typing import List, Dict
19
-
20
- from .identity.keygen import KeyManager
21
- from .identity.manifest import AgentManifest, ManifestValidator
22
-
23
- logger = logging.getLogger("NUKE_PROTOCOL")
24
-
25
- class NukePacker:
26
- """
27
- Creates signed .nuke artifacts.
28
- """
29
- def __init__(self, staging_root: Path):
30
- self.staging_root = staging_root
31
- self.staging_root.mkdir(parents=True, exist_ok=True)
32
- self.key_manager = KeyManager()
33
-
34
- def pack(self, manifest: AgentManifest, tool_paths: List[Path], private_key_pem: str, output_path: Path) -> Path:
35
- """
36
- Bundles an agent into a .nuke file.
37
- """
38
- logger.info(f"📦 Packing Agent: {manifest.agent.id} -> {output_path}")
39
-
40
- # 1. Create Staging Area
41
- with tempfile.TemporaryDirectory(dir=self.staging_root) as tmp_dir:
42
- staging_dir = Path(tmp_dir)
43
- tools_dir = staging_dir / "tools"
44
- tools_dir.mkdir()
45
-
46
- # 2. Add Tools
47
- for tool_path in tool_paths:
48
- if not tool_path.exists():
49
- raise FileNotFoundError(f"Tool not found: {tool_path}")
50
- shutil.copy2(tool_path, tools_dir / tool_path.name)
51
-
52
- # 3. Write Manifest
53
- manifest_json = manifest.model_dump_json(indent=2)
54
- manifest_path = staging_dir / "manifest.json"
55
- manifest_path.write_text(manifest_json)
56
-
57
- # 4. Sign Manifest
58
- # We sign the BYTES of the manifest.json file to ensure exact integrity
59
- manifest_bytes = manifest_path.read_bytes()
60
- signature = self.key_manager.sign(private_key_pem, manifest_bytes)
61
-
62
- # Save signature as hex string
63
- (staging_dir / "signature.sig").write_text(signature.hex())
64
-
65
- # 5. Zip it
66
- with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
67
- for file in staging_dir.rglob("*"):
68
- if file.is_file():
69
- zf.write(file, file.relative_to(staging_dir))
70
-
71
- logger.info(f"✅ Created {output_path} ({output_path.stat().st_size} bytes)")
72
- return output_path
73
-
74
- class NukeLoader:
75
- """
76
- Loads and Verifies .nuke artifacts.
77
- """
78
- def __init__(self, install_root: Path):
79
- self.install_root = install_root
80
- self.key_manager = KeyManager()
81
- self.installed_tools_dir = install_root / "tools" / "installed"
82
- self.installed_tools_dir.mkdir(parents=True, exist_ok=True)
83
-
84
- def load(self, nuke_path: Path, trusted_keys: Dict[str, str]) -> AgentManifest:
85
- """
86
- Verifies and Unpacks a .nuke file.
87
- trusted_keys: Dict[KeyID, PublicKeyPEM]
88
- """
89
- logger.info(f"🔓 Loading .nuke: {nuke_path}")
90
-
91
- with zipfile.ZipFile(nuke_path, 'r') as zf:
92
- # 1. Extract Manifest & Sig to memory
93
- try:
94
- manifest_bytes = zf.read("manifest.json")
95
- signature_hex = zf.read("signature.sig").decode().strip()
96
- signature_bytes = bytes.fromhex(signature_hex)
97
- except KeyError:
98
- raise ValueError("❌ Invalid .nuke: Missing manifest or signature")
99
-
100
- # 2. Parse Manifest
101
- try:
102
- manifest_dict = json.loads(manifest_bytes)
103
- manifest = ManifestValidator.validate(manifest_dict)
104
- except Exception as e:
105
- raise ValueError(f"❌ Invalid Manifest: {e}")
106
-
107
- # 3. Lookup Public Key
108
- # In Phase 57, we assume the publisher ID is embedded in the manifest or we trust the signer
109
- # For this MVP, we verify using the TRUSTED_KEYS passed in.
110
- # We treat the Key ID as the signer.
111
-
112
- # Note: The Manifest doesn't strictly have a "signer_id" field in our schema yet.
113
- # We rely on the KeyManager logic: We need the Public Key corresponding to the Private Key that signed it.
114
- # In a real PKI, the Manifest would contain the KeyID, or the Repository would provide it.
115
- # Let's try every trusted key. If ANY valid key verifies it, we are good.
116
-
117
- verified = False
118
- signer_key_id = None
119
-
120
- for key_id, pub_pem in trusted_keys.items():
121
- if self.key_manager.verify(pub_pem, signature_bytes, manifest_bytes):
122
- verified = True
123
- signer_key_id = key_id
124
- break
125
-
126
- if not verified:
127
- raise PermissionError("❌ SIGNATURE VERIFICATION FAILED: No trusted key matches signature")
128
-
129
- logger.info(f"✅ Verified by KeyID: {signer_key_id}")
130
-
131
- # 4. Install Tools
132
- # We namespace them by Agent ID to avoid collisions
133
- agent_dir = self.installed_tools_dir / manifest.agent.id
134
- if agent_dir.exists():
135
- shutil.rmtree(agent_dir)
136
- agent_dir.mkdir()
137
-
138
- for file in zf.namelist():
139
- if file.startswith("tools/") and file.endswith(".py"):
140
- # Extract tool code
141
- tool_content = zf.read(file)
142
- tool_name = Path(file).name
143
- (agent_dir / tool_name).write_bytes(tool_content)
144
-
145
- logger.info(f" -> Installed {len(list(agent_dir.glob('*.py')))} tools to {agent_dir}")
146
-
147
- return manifest