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,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
|