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,286 +0,0 @@
1
- """
2
- Nucleus Runtime - Prometheus Metrics (AG-015)
3
- ==============================================
4
- Prometheus-compatible metrics endpoint for monitoring Nucleus.
5
-
6
- Usage:
7
- # In your monitoring setup, scrape:
8
- # GET /metrics (if HTTP server enabled)
9
- # OR call brain_prometheus_metrics() MCP tool
10
-
11
- # Metrics exported:
12
- # - nucleus_tool_calls_total{tool="brain_health"}
13
- # - nucleus_tool_errors_total{tool="brain_health"}
14
- # - nucleus_tool_latency_seconds{tool="brain_health",quantile="0.5"}
15
- # - nucleus_tasks_total{status="PENDING"}
16
- # - nucleus_sessions_total
17
- # - nucleus_events_total
18
- """
19
-
20
- import time
21
- import threading
22
- from typing import Dict, Any, List
23
- from datetime import datetime, timezone
24
- from collections import defaultdict
25
- import os
26
- import json
27
- from pathlib import Path
28
-
29
- # Thread-safe metrics storage
30
- _metrics_lock = threading.Lock()
31
-
32
- # Counters
33
- _tool_calls: Dict[str, int] = defaultdict(int)
34
- _tool_errors: Dict[str, int] = defaultdict(int)
35
-
36
- # Histograms (simplified - store recent latencies)
37
- _tool_latencies: Dict[str, List[float]] = defaultdict(list)
38
- MAX_LATENCY_SAMPLES = 1000 # Keep last N samples per tool
39
-
40
- # Gauges (current values)
41
- _gauges: Dict[str, float] = {}
42
-
43
- # Configuration
44
- METRICS_ENABLED = os.environ.get("NUCLEUS_METRICS", "true").lower() == "true"
45
-
46
-
47
- def inc_counter(name: str, labels: Dict[str, str] = None, value: int = 1):
48
- """Increment a counter metric."""
49
- if not METRICS_ENABLED:
50
- return
51
-
52
- key = _make_key(name, labels)
53
- with _metrics_lock:
54
- if name.endswith("_errors"):
55
- _tool_errors[key] += value
56
- else:
57
- _tool_calls[key] += value
58
-
59
-
60
- def observe_latency(tool_name: str, latency_seconds: float):
61
- """Record a latency observation for a tool."""
62
- if not METRICS_ENABLED:
63
- return
64
-
65
- with _metrics_lock:
66
- latencies = _tool_latencies[tool_name]
67
- latencies.append(latency_seconds)
68
- # Keep bounded
69
- if len(latencies) > MAX_LATENCY_SAMPLES:
70
- _tool_latencies[tool_name] = latencies[-MAX_LATENCY_SAMPLES:]
71
-
72
-
73
- def set_gauge(name: str, value: float, labels: Dict[str, str] = None):
74
- """Set a gauge metric to a specific value."""
75
- if not METRICS_ENABLED:
76
- return
77
-
78
- key = _make_key(name, labels)
79
- with _metrics_lock:
80
- _gauges[key] = value
81
-
82
-
83
- def _make_key(name: str, labels: Dict[str, str] = None) -> str:
84
- """Create a metric key from name and labels."""
85
- if not labels:
86
- return name
87
- label_str = ",".join(f'{k}="{v}"' for k, v in sorted(labels.items()))
88
- return f"{name}{{{label_str}}}"
89
-
90
-
91
- def _calculate_quantiles(values: List[float], quantiles: List[float] = [0.5, 0.9, 0.99]) -> Dict[str, float]:
92
- """Calculate quantiles from a list of values."""
93
- if not values:
94
- return {str(q): 0.0 for q in quantiles}
95
-
96
- sorted_values = sorted(values)
97
- n = len(sorted_values)
98
- result = {}
99
-
100
- for q in quantiles:
101
- idx = int(q * n)
102
- if idx >= n:
103
- idx = n - 1
104
- result[str(q)] = sorted_values[idx]
105
-
106
- return result
107
-
108
-
109
- def get_prometheus_metrics() -> str:
110
- """
111
- Generate Prometheus text format metrics.
112
-
113
- Returns:
114
- Prometheus exposition format string
115
- """
116
- lines = []
117
-
118
- # Header
119
- lines.append("# Nucleus MCP Server Metrics")
120
- lines.append(f"# Generated at {datetime.now(timezone.utc).isoformat()}")
121
- lines.append("")
122
-
123
- with _metrics_lock:
124
- # Tool call counters
125
- lines.append("# HELP nucleus_tool_calls_total Total number of tool calls")
126
- lines.append("# TYPE nucleus_tool_calls_total counter")
127
- for key, value in _tool_calls.items():
128
- lines.append(f"nucleus_tool_calls_total{{{_extract_labels(key)}}} {value}")
129
-
130
- # Tool error counters
131
- lines.append("")
132
- lines.append("# HELP nucleus_tool_errors_total Total number of tool errors")
133
- lines.append("# TYPE nucleus_tool_errors_total counter")
134
- for key, value in _tool_errors.items():
135
- lines.append(f"nucleus_tool_errors_total{{{_extract_labels(key)}}} {value}")
136
-
137
- # Latency histograms (as summary with quantiles)
138
- lines.append("")
139
- lines.append("# HELP nucleus_tool_latency_seconds Tool execution latency")
140
- lines.append("# TYPE nucleus_tool_latency_seconds summary")
141
- for tool_name, latencies in _tool_latencies.items():
142
- if latencies:
143
- quantiles = _calculate_quantiles(latencies)
144
- for q, v in quantiles.items():
145
- lines.append(f'nucleus_tool_latency_seconds{{tool="{tool_name}",quantile="{q}"}} {v:.6f}')
146
- lines.append(f'nucleus_tool_latency_seconds_sum{{tool="{tool_name}"}} {sum(latencies):.6f}')
147
- lines.append(f'nucleus_tool_latency_seconds_count{{tool="{tool_name}"}} {len(latencies)}')
148
-
149
- # Gauges
150
- lines.append("")
151
- lines.append("# HELP nucleus_gauge Current gauge values")
152
- lines.append("# TYPE nucleus_gauge gauge")
153
- for key, value in _gauges.items():
154
- lines.append(f"nucleus_gauge{{{_extract_labels(key)}}} {value}")
155
-
156
- # Add brain state metrics if available
157
- try:
158
- brain_path = Path(os.environ.get("NUCLEAR_BRAIN_PATH", ".brain"))
159
-
160
- # Task counts by status
161
- tasks_file = brain_path / "ledger" / "tasks.json"
162
- if tasks_file.exists():
163
- tasks = json.loads(tasks_file.read_text())
164
- status_counts = defaultdict(int)
165
- for task in tasks:
166
- status_counts[task.get("status", "UNKNOWN")] += 1
167
-
168
- lines.append("")
169
- lines.append("# HELP nucleus_tasks_total Number of tasks by status")
170
- lines.append("# TYPE nucleus_tasks_total gauge")
171
- for status, count in status_counts.items():
172
- lines.append(f'nucleus_tasks_total{{status="{status}"}} {count}')
173
-
174
- # Session count
175
- sessions_dir = brain_path / "sessions"
176
- if sessions_dir.exists():
177
- session_count = len(list(sessions_dir.glob("*.json")))
178
- lines.append("")
179
- lines.append("# HELP nucleus_sessions_total Total saved sessions")
180
- lines.append("# TYPE nucleus_sessions_total gauge")
181
- lines.append(f"nucleus_sessions_total {session_count}")
182
-
183
- # Event count
184
- events_file = brain_path / "ledger" / "events.jsonl"
185
- if events_file.exists():
186
- with open(events_file) as f:
187
- event_count = sum(1 for _ in f)
188
- lines.append("")
189
- lines.append("# HELP nucleus_events_total Total events logged")
190
- lines.append("# TYPE nucleus_events_total gauge")
191
- lines.append(f"nucleus_events_total {event_count}")
192
-
193
- except Exception:
194
- pass # Silently skip brain metrics if unavailable
195
-
196
- lines.append("")
197
- return "\n".join(lines)
198
-
199
-
200
- def _extract_labels(key: str) -> str:
201
- """Extract labels from a metric key."""
202
- if "{" in key:
203
- start = key.index("{")
204
- end = key.rindex("}")
205
- return key[start+1:end]
206
- return f'name="{key}"'
207
-
208
-
209
- def reset_metrics():
210
- """Reset all metrics (useful for testing)."""
211
- global _tool_calls, _tool_errors, _tool_latencies, _gauges
212
- with _metrics_lock:
213
- _tool_calls = defaultdict(int)
214
- _tool_errors = defaultdict(int)
215
- _tool_latencies = defaultdict(list)
216
- _gauges = {}
217
-
218
-
219
- def get_metrics_json() -> Dict[str, Any]:
220
- """Get metrics as JSON (alternative to Prometheus format)."""
221
- with _metrics_lock:
222
- latency_stats = {}
223
- for tool_name, latencies in _tool_latencies.items():
224
- if latencies:
225
- latency_stats[tool_name] = {
226
- "count": len(latencies),
227
- "sum": sum(latencies),
228
- "avg": sum(latencies) / len(latencies),
229
- "quantiles": _calculate_quantiles(latencies)
230
- }
231
-
232
- return {
233
- "tool_calls": dict(_tool_calls),
234
- "tool_errors": dict(_tool_errors),
235
- "latencies": latency_stats,
236
- "gauges": dict(_gauges),
237
- "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
238
- }
239
-
240
-
241
- # Decorator for automatic metrics collection
242
- def metrics_tracked(tool_name: str):
243
- """
244
- Decorator to automatically track tool metrics.
245
-
246
- Usage:
247
- @metrics_tracked("brain_health")
248
- def brain_health():
249
- ...
250
- """
251
- def decorator(func):
252
- import functools
253
-
254
- @functools.wraps(func)
255
- def wrapper(*args, **kwargs):
256
- start = time.perf_counter()
257
- try:
258
- result = func(*args, **kwargs)
259
- inc_counter("tool_calls", {"tool": tool_name})
260
- return result
261
- except Exception:
262
- inc_counter("tool_errors", {"tool": tool_name})
263
- raise
264
- finally:
265
- latency = time.perf_counter() - start
266
- observe_latency(tool_name, latency)
267
-
268
- @functools.wraps(func)
269
- async def async_wrapper(*args, **kwargs):
270
- start = time.perf_counter()
271
- try:
272
- result = await func(*args, **kwargs)
273
- inc_counter("tool_calls", {"tool": tool_name})
274
- return result
275
- except Exception:
276
- inc_counter("tool_errors", {"tool": tool_name})
277
- raise
278
- finally:
279
- latency = time.perf_counter() - start
280
- observe_latency(tool_name, latency)
281
-
282
- import inspect
283
- if inspect.iscoroutinefunction(func):
284
- return async_wrapper
285
- return wrapper
286
- return decorator
@@ -1,101 +0,0 @@
1
-
2
- """
3
- ProposalOps: Deep Thought & Ratification Engine.
4
- Allows the Daemon to "Think before it Acts" for high-stakes decisions.
5
-
6
- Strategic Role:
7
- - Generates "Strategic Briefings" (Option A vs B).
8
- - Handles "Ratification" (User Approval) for Unshackled actions.
9
- - Updates Trust Scores based on successful outcomes.
10
- """
11
-
12
- import json
13
- from datetime import datetime
14
- from pathlib import Path
15
- from typing import List, Dict, Optional
16
- from dataclasses import dataclass, asdict
17
-
18
- # Re-use our locking mechanism for safety
19
- from .locking import get_lock
20
-
21
- @dataclass
22
- class Proposal:
23
- id: str
24
- title: str
25
- description: str
26
- options: List[Dict[str, str]] # [{"id": "A", "desc": "Do X"}, {"id": "B", "desc": "Do Y"}]
27
- risk_level: str # "low", "medium", "high", "critical"
28
- created_at: str
29
- status: str = "pending" # pending, ratified, rejected, executed
30
- ratified_option_id: Optional[str] = None
31
-
32
- class ProposalOps:
33
- def __init__(self, brain_path: Path):
34
- self.brain_path = brain_path
35
- self.proposals_dir = brain_path / "ledger" / "proposals"
36
- self.proposals_dir.mkdir(parents=True, exist_ok=True)
37
-
38
- def generate_proposal(self, title: str, description: str, options: List[Dict], risk_level: str = "medium") -> Proposal:
39
- """Create a new decision proposal"""
40
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
41
- pid = f"prop_{timestamp}"
42
-
43
- proposal = Proposal(
44
- id=pid,
45
- title=title,
46
- description=description,
47
- options=options,
48
- risk_level=risk_level,
49
- created_at=datetime.now().isoformat()
50
- )
51
-
52
- self._save_proposal(proposal)
53
- return proposal
54
-
55
- def _save_proposal(self, proposal: Proposal):
56
- """Save proposal atomically"""
57
- file_path = self.proposals_dir / f"{proposal.id}.json"
58
-
59
- with get_lock("proposals", self.brain_path).section():
60
- with open(file_path, 'w') as f:
61
- json.dump(asdict(proposal), f, indent=2)
62
-
63
- def get_pending_proposals(self) -> List[Proposal]:
64
- """List all pending proposals"""
65
- pending = []
66
- # No lock needed for simple list unless we want strict consistency
67
- if not self.proposals_dir.exists():
68
- return []
69
-
70
- for f in self.proposals_dir.glob("prop_*.json"):
71
- try:
72
- data = json.loads(f.read_text())
73
- if data.get("status") == "pending":
74
- pending.append(Proposal(**data))
75
- except Exception:
76
- pass
77
- return pending
78
-
79
- def ratify_proposal(self, proposal_id: str, option_id: str) -> bool:
80
- """User validates a proposal"""
81
- with get_lock("proposals", self.brain_path).section():
82
- file_path = self.proposals_dir / f"{proposal_id}.json"
83
- if not file_path.exists():
84
- return False
85
-
86
- data = json.loads(file_path.read_text())
87
- if data["status"] != "pending":
88
- return False
89
-
90
- # Verify option exists
91
- if not any(opt["id"] == option_id for opt in data["options"]):
92
- return False
93
-
94
- data["status"] = "ratified"
95
- data["ratified_option_id"] = option_id
96
- data["ratified_at"] = datetime.now().isoformat()
97
-
98
- with open(file_path, 'w') as f:
99
- json.dump(data, f, indent=2)
100
-
101
- return True
@@ -1,109 +0,0 @@
1
- """
2
- Publisher: The Shipping Department.
3
- Orchestrates the creation of Signed Agent Artifacts.
4
-
5
- Strategic Role:
6
- - VALIDATOR: Ensures Manifest is legal before packing.
7
- - SIGNER: Applies the Keymaster's seal.
8
- - PACKER: Creates the purely immutable .nuke file.
9
- """
10
-
11
- import logging
12
- from pathlib import Path
13
- from typing import Optional
14
-
15
- from .identity.keygen import KeyManager
16
- from .identity.manifest import ManifestValidator
17
- from .nuke_protocol import NukePacker
18
-
19
- logger = logging.getLogger("PUBLISHER")
20
-
21
- class Publisher:
22
- def __init__(self, brain_path: Path, key_manager: Optional[KeyManager] = None):
23
- self.brain_path = brain_path
24
- self.key_manager = key_manager or KeyManager(brain_path)
25
- self.packer = NukePacker(staging_root=brain_path / "staging")
26
- self.validator = ManifestValidator()
27
-
28
- def publish(self, agent_source: Path, output_dir: Path, key_id: str, private: bool = False) -> Path:
29
- """
30
- Publish an agent from source directory.
31
-
32
- Args:
33
- agent_source: Directory containing manifest.json and code.
34
- output_dir: Where to save the .nuke artifact.
35
- key_id: ID of the key to sign with.
36
- private: If True, marks manifest as private (if supported).
37
-
38
- Returns:
39
- Path to the generated .nuke file.
40
- """
41
- agent_source = Path(agent_source)
42
- output_dir = Path(output_dir)
43
- output_dir.mkdir(parents=True, exist_ok=True)
44
-
45
- logger.info(f"📦 Publishing agent from {agent_source}...")
46
-
47
- # 1. Validate Manifest
48
- manifest_path = agent_source / "manifest.json"
49
- if not manifest_path.exists():
50
- raise FileNotFoundError("manifest.json missing in source")
51
-
52
- manifest_data = manifest_path.read_text()
53
- import json
54
- manifest_obj = json.loads(manifest_data)
55
- if not self.validator.validate(manifest_obj):
56
- raise ValueError("❌ Validation Failed: User's manifest is invalid.")
57
-
58
- # 2. Get Signature
59
- # We need to sign the content. NukePacker.pack signs the manifest.
60
- # Wait, NukePacker.pack takes a private_key_pem.
61
-
62
- try:
63
- # Retrieve private key protected by KeyManager
64
- # In Phase 57, KeyManager.get_key returns KeyPair (has private bytes)
65
- # But get_key might return None.
66
- # actually we don't have get_key exposed publicly easily without password?
67
- # KeyManager.sign is the preferred way?
68
- # NukePacker pack expects private_key_pem string.
69
-
70
- # Let's peek at KeyManager implementation via memory or usage.
71
- # It loads from disk.
72
- # We'll use key_manager._load_key_pair(key_id) which is internal?
73
- # Or assume KeyManager is initialized.
74
-
75
- # Better: KeyManager SHOULD handle signing. NukePacker should take a signer callback?
76
- # For Phase 57, let's load the PEM.
77
-
78
- key_pair = self.key_manager.get_key_pair(key_id) # Assuming this exists or we need to add it
79
- if not key_pair:
80
- raise ValueError(f"Key ID {key_id} not found.")
81
-
82
- private_key_pem = key_pair.private_key_pem
83
-
84
- # 3. Collect Tools
85
- # Naive scan for now: all .py files in source except manifest
86
- tool_paths = list(agent_source.glob("*.py"))
87
-
88
- # 4. Pack & Sign
89
- # Construct output filename: agent.id-version.nuke
90
- # We must use the VALIDATED manifest object
91
- from .identity.manifest import AgentManifest
92
- validated_manifest = AgentManifest(**manifest_obj)
93
-
94
- artifact_filename = f"{validated_manifest.agent.id}-{validated_manifest.agent.version}.nuke"
95
- final_artifact_path = output_dir / artifact_filename
96
-
97
- self.packer.pack(
98
- manifest=validated_manifest,
99
- tool_paths=tool_paths,
100
- private_key_pem=private_key_pem,
101
- output_path=final_artifact_path
102
- )
103
-
104
- logger.info(f"✅ Published: {final_artifact_path}")
105
- return final_artifact_path
106
-
107
- except Exception as e:
108
- logger.error(f"❌ Publishing failed: {e}")
109
- raise e