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,81 +0,0 @@
1
- """
2
- Installer: The Bridge Builder.
3
- Connects Artifacts to the Runtime.
4
-
5
- Roles:
6
- 1. ORCHESTRATION: Coordinates Loading, Verification, and Registration.
7
- 2. TRUST ENFORCEMENT: Fetches the Team's Trusted Roots for verification.
8
- 3. LIFECYCLE HANDOFF: Registers the successfully installed agent.
9
- """
10
-
11
- import logging
12
- from pathlib import Path
13
- from typing import Optional
14
-
15
- from .nuke_protocol import NukeLoader
16
- from .lifecycle import LifecycleManager
17
- from .identity.manifest import AgentManifest
18
- from .team import TeamManager
19
-
20
- logger = logging.getLogger("INSTALLER")
21
-
22
- class Installer:
23
- def __init__(self, brain_path: Path):
24
- self.brain_path = brain_path
25
- self.lifecycle = LifecycleManager(brain_path)
26
- self.team = TeamManager(brain_path)
27
- # NukeLoader installs into <brain>/agents/
28
- # Wait, NukeLoader takes `install_root`.
29
- # By convention, agents live in <brain>/agents/installed?
30
- # mcp_server_nucleus typically has agents in .brain/agents.
31
- # Let's use <brain_path> as install root so tools go into <brain>/tools/installed
32
- # But `Factory` might expect them elsewhere.
33
- # Let's align with NukeLoader's default: install_root/tools/installed/<id>
34
- self.loader = NukeLoader(brain_path)
35
-
36
- def install_from_file(self, nuke_path: Path) -> Optional[AgentManifest]:
37
- """
38
- Installs an agent from a local .nuke artifact.
39
-
40
- Steps:
41
- 1. Load Team's Trusted Keys.
42
- 2. Unpack & Verify Protocol (NukeLoader).
43
- 3. Register with LifecycleManager.
44
- """
45
- try:
46
- logger.info("Step 1: Fetching Trust Roots...")
47
- trusted_keys_list = self.team.get_trusted_roots()
48
-
49
- # NukeLoader expects Dict[KeyID, PEM]
50
- # TeamManager returns List[KeyFingerprint] (Strings).
51
- # Wait, `verify_team` tested `get_trusted_roots` returns `config.trusted_keys`.
52
- # In `publisher.py` (Chat 25), checking verify_publisher...
53
- # The signature verification needs the PUBLIC KEY PEM.
54
- # `TeamConfig` stores... strings.
55
- # Ideally `TeamConfig` would store a mapping of "Name/ID" -> "Public Key PEM".
56
- # Or assume the keys are stored in a KeyStore.
57
-
58
- # For Phase 57 MVP, let's assume `trusted_keys` in TeamConfig is a DICT check?
59
- # Looking at `verify_team.py`, `trusted_keys` was a list.
60
- # Looking at `nuke_protocol.py`, `load` takes `trusted_keys: Dict[str, str]`.
61
-
62
- # FIX:
63
- # TeamManager stores `trusted_keys` as a List[str].
64
- # NukeLoader expects `Dict[key_id, pub_pem]`.
65
- # For MVP, we map the list to generated IDs or use the list index.
66
- # In a real system, the KeyStore would map KeyID -> PEM.
67
-
68
- trusted_map = {f"trusted_{i}": key for i, key in enumerate(trusted_keys_list)}
69
-
70
- logger.info(f"Step 2: Installing {nuke_path}...")
71
- manifest = self.loader.load(nuke_path, trusted_map)
72
-
73
- logger.info(f"Step 3: Registering {manifest.agent.id}...")
74
- self.lifecycle.register_agent(manifest.agent.id)
75
-
76
- logger.info(f"✅ Successfully Installed: {manifest.agent.name} ({manifest.agent.id})")
77
- return manifest
78
-
79
- except Exception as e:
80
- logger.error(f"❌ Installation Failed: {e}")
81
- raise e
@@ -1,377 +0,0 @@
1
- """
2
- v0.6.0 DSoR: IPC Authentication & Token Metering
3
-
4
- Remediates CVE-2026-001 (Sidecar Exploit) from V9_VULNERABILITY_REPORT.md:
5
- - Implements per-request IPC auth tokens (no more implicit trust)
6
- - Links token metering to DecisionMade events (prevents billing bypass)
7
-
8
- Security Model:
9
- - Every IPC request must include a short-lived auth token
10
- - Tokens are bound to specific decision_ids for audit linkage
11
- - Token consumption is metered and logged for billing accuracy
12
- """
13
-
14
- import hashlib
15
- import hmac
16
- import json
17
- import os
18
- import secrets
19
- from dataclasses import dataclass, field, asdict
20
- from datetime import datetime, timezone
21
- from pathlib import Path
22
- from typing import Dict, Any, List, Optional
23
-
24
-
25
- def get_brain_path() -> Path:
26
- """Get the brain path from environment."""
27
- return Path(os.getenv("NUCLEAR_BRAIN_PATH", "/Users/lokeshgarg/ai-mvp-backend/.brain"))
28
-
29
-
30
- @dataclass
31
- class IPCToken:
32
- """
33
- Per-request IPC authentication token.
34
- Short-lived, single-use, bound to a decision.
35
- """
36
- token_id: str
37
- decision_id: Optional[str] # Linked DecisionMade event
38
- created_at: str
39
- expires_at: str
40
- scope: str # e.g., "tool_call", "read", "write"
41
- consumed: bool = False
42
- consumed_at: Optional[str] = None
43
- request_hash: Optional[str] = None # Hash of the request this token authorized
44
-
45
- def to_dict(self) -> Dict[str, Any]:
46
- return asdict(self)
47
-
48
- @classmethod
49
- def from_dict(cls, data: Dict[str, Any]) -> "IPCToken":
50
- return cls(**data)
51
-
52
- def is_valid(self) -> bool:
53
- """Check if token is still valid (not expired, not consumed)."""
54
- if self.consumed:
55
- return False
56
- now = datetime.now(timezone.utc).isoformat()
57
- return now < self.expires_at
58
-
59
-
60
- @dataclass
61
- class TokenMeterEntry:
62
- """
63
- Metering entry for billing and audit.
64
- Links token consumption to decisions and resource usage.
65
- """
66
- entry_id: str
67
- token_id: str
68
- decision_id: Optional[str]
69
- timestamp: str
70
- scope: str
71
- resource_type: str # e.g., "tool_call", "llm_tokens", "compute"
72
- units_consumed: float
73
- metadata: Dict[str, Any] = field(default_factory=dict)
74
-
75
- def to_dict(self) -> Dict[str, Any]:
76
- return asdict(self)
77
-
78
-
79
- class IPCAuthManager:
80
- """
81
- v0.6.0 DSoR: Manages IPC authentication and token metering.
82
-
83
- Addresses V9 vulnerabilities:
84
- 1. CVE-2026-001: Per-request auth prevents socket impersonation
85
- 2. Pricing Rebellion: Metering linked to DecisionMade prevents bypass
86
- """
87
-
88
- # Token validity duration in seconds
89
- TOKEN_TTL_SECONDS = 30 # Short-lived for security
90
-
91
- def __init__(self, brain_path: Optional[Path] = None):
92
- self.brain_path = brain_path or get_brain_path()
93
- self._active_tokens: Dict[str, IPCToken] = {}
94
- self._secret_key: Optional[bytes] = None
95
- self._meter_log: List[TokenMeterEntry] = []
96
- self._load_or_create_secret()
97
-
98
- def _load_or_create_secret(self) -> None:
99
- """Load or create the IPC secret key."""
100
- secrets_dir = self.brain_path / "secrets"
101
- secrets_dir.mkdir(parents=True, exist_ok=True)
102
-
103
- key_file = secrets_dir / ".ipc_secret"
104
-
105
- if key_file.exists():
106
- self._secret_key = key_file.read_bytes()
107
- else:
108
- # Generate new secret (32 bytes = 256 bits)
109
- self._secret_key = secrets.token_bytes(32)
110
- key_file.write_bytes(self._secret_key)
111
- # Restrict permissions (owner read/write only)
112
- os.chmod(key_file, 0o600)
113
-
114
- def _generate_token_id(self) -> str:
115
- """Generate a unique token ID."""
116
- return f"ipc-{secrets.token_hex(12)}"
117
-
118
- def _compute_token_signature(self, token_id: str, scope: str,
119
- decision_id: Optional[str]) -> str:
120
- """Compute HMAC signature for token validation."""
121
- message = f"{token_id}:{scope}:{decision_id or 'none'}"
122
- signature = hmac.new(
123
- self._secret_key,
124
- message.encode(),
125
- hashlib.sha256
126
- ).hexdigest()[:16]
127
- return signature
128
-
129
- def issue_token(self, scope: str, decision_id: Optional[str] = None,
130
- ttl_seconds: Optional[int] = None) -> IPCToken:
131
- """
132
- Issue a new per-request IPC auth token.
133
-
134
- Args:
135
- scope: The scope of operations this token authorizes
136
- decision_id: Optional linked DecisionMade event ID
137
- ttl_seconds: Optional custom TTL (defaults to TOKEN_TTL_SECONDS)
138
-
139
- Returns:
140
- New IPCToken instance
141
- """
142
- ttl = ttl_seconds or self.TOKEN_TTL_SECONDS
143
- now = datetime.now(timezone.utc)
144
-
145
- token_id = self._generate_token_id()
146
- expires_at = datetime.fromtimestamp(
147
- now.timestamp() + ttl,
148
- tz=timezone.utc
149
- ).isoformat()
150
-
151
- token = IPCToken(
152
- token_id=token_id,
153
- decision_id=decision_id,
154
- created_at=now.isoformat(),
155
- expires_at=expires_at,
156
- scope=scope
157
- )
158
-
159
- self._active_tokens[token_id] = token
160
-
161
- # Log token issuance
162
- self._log_token_event("issued", token)
163
-
164
- return token
165
-
166
- def validate_token(self, token_id: str, scope: str,
167
- request_hash: Optional[str] = None) -> tuple[bool, str]:
168
- """
169
- Validate an IPC token for a request.
170
-
171
- Args:
172
- token_id: The token ID to validate
173
- scope: The required scope for the operation
174
- request_hash: Optional hash of the request being authorized
175
-
176
- Returns:
177
- Tuple of (is_valid, error_message)
178
- """
179
- # Check token exists
180
- token = self._active_tokens.get(token_id)
181
- if not token:
182
- return False, "Token not found or expired"
183
-
184
- # Check not consumed
185
- if token.consumed:
186
- return False, "Token already consumed (single-use)"
187
-
188
- # Check not expired
189
- now = datetime.now(timezone.utc).isoformat()
190
- if now >= token.expires_at:
191
- return False, "Token expired"
192
-
193
- # Check scope matches
194
- if token.scope != scope and token.scope != "admin":
195
- return False, f"Scope mismatch: token has '{token.scope}', need '{scope}'"
196
-
197
- return True, "Valid"
198
-
199
- def consume_token(self, token_id: str, request_hash: Optional[str] = None,
200
- resource_type: str = "tool_call",
201
- units: float = 1.0,
202
- metadata: Optional[Dict[str, Any]] = None) -> bool:
203
- """
204
- Consume a token and record metering entry.
205
-
206
- Args:
207
- token_id: The token to consume
208
- request_hash: Hash of the request this token authorized
209
- resource_type: Type of resource being consumed
210
- units: Number of units consumed (for billing)
211
- metadata: Optional additional metadata
212
-
213
- Returns:
214
- True if token was successfully consumed
215
- """
216
- token = self._active_tokens.get(token_id)
217
- if not token or token.consumed:
218
- return False
219
-
220
- # Mark as consumed
221
- token.consumed = True
222
- token.consumed_at = datetime.now(timezone.utc).isoformat()
223
- token.request_hash = request_hash
224
-
225
- # Create metering entry
226
- meter_entry = TokenMeterEntry(
227
- entry_id=f"meter-{secrets.token_hex(8)}",
228
- token_id=token_id,
229
- decision_id=token.decision_id,
230
- timestamp=token.consumed_at,
231
- scope=token.scope,
232
- resource_type=resource_type,
233
- units_consumed=units,
234
- metadata=metadata or {}
235
- )
236
-
237
- self._meter_log.append(meter_entry)
238
-
239
- # Persist metering entry
240
- self._persist_meter_entry(meter_entry)
241
-
242
- # Log consumption
243
- self._log_token_event("consumed", token)
244
-
245
- return True
246
-
247
- def _persist_meter_entry(self, entry: TokenMeterEntry) -> None:
248
- """Persist metering entry to ledger."""
249
- try:
250
- meter_dir = self.brain_path / "ledger" / "metering"
251
- meter_dir.mkdir(parents=True, exist_ok=True)
252
-
253
- meter_file = meter_dir / "token_meter.jsonl"
254
- with open(meter_file, "a") as f:
255
- f.write(json.dumps(entry.to_dict()) + "\n")
256
- except Exception:
257
- pass # Non-blocking
258
-
259
- def _log_token_event(self, event_type: str, token: IPCToken) -> None:
260
- """Log token lifecycle event."""
261
- try:
262
- auth_dir = self.brain_path / "ledger" / "auth"
263
- auth_dir.mkdir(parents=True, exist_ok=True)
264
-
265
- log_file = auth_dir / "ipc_tokens.jsonl"
266
- event = {
267
- "event": event_type,
268
- "token_id": token.token_id,
269
- "decision_id": token.decision_id,
270
- "scope": token.scope,
271
- "timestamp": datetime.now(timezone.utc).isoformat()
272
- }
273
- with open(log_file, "a") as f:
274
- f.write(json.dumps(event) + "\n")
275
- except Exception:
276
- pass # Non-blocking
277
-
278
- def cleanup_expired_tokens(self) -> int:
279
- """
280
- Clean up expired tokens from memory.
281
-
282
- Returns:
283
- Number of tokens cleaned up
284
- """
285
- now = datetime.now(timezone.utc).isoformat()
286
- expired = [
287
- tid for tid, token in self._active_tokens.items()
288
- if now >= token.expires_at
289
- ]
290
-
291
- for tid in expired:
292
- del self._active_tokens[tid]
293
-
294
- return len(expired)
295
-
296
- def get_metering_summary(self, since: Optional[str] = None) -> Dict[str, Any]:
297
- """
298
- Get metering summary for billing/audit.
299
-
300
- Args:
301
- since: Optional ISO timestamp to filter from
302
-
303
- Returns:
304
- Summary of token consumption and metering
305
- """
306
- entries = self._meter_log
307
- if since:
308
- entries = [e for e in entries if e.timestamp >= since]
309
-
310
- summary = {
311
- "total_entries": len(entries),
312
- "total_units": sum(e.units_consumed for e in entries),
313
- "by_scope": {},
314
- "by_resource_type": {},
315
- "decisions_linked": 0
316
- }
317
-
318
- for entry in entries:
319
- # By scope
320
- if entry.scope not in summary["by_scope"]:
321
- summary["by_scope"][entry.scope] = 0
322
- summary["by_scope"][entry.scope] += entry.units_consumed
323
-
324
- # By resource type
325
- if entry.resource_type not in summary["by_resource_type"]:
326
- summary["by_resource_type"][entry.resource_type] = 0
327
- summary["by_resource_type"][entry.resource_type] += entry.units_consumed
328
-
329
- # Count linked decisions
330
- if entry.decision_id:
331
- summary["decisions_linked"] += 1
332
-
333
- return summary
334
-
335
-
336
- # Singleton instance
337
- _ipc_auth_manager: Optional[IPCAuthManager] = None
338
-
339
-
340
- def get_ipc_auth_manager() -> IPCAuthManager:
341
- """Get or create the singleton IPCAuthManager instance."""
342
- global _ipc_auth_manager
343
- if _ipc_auth_manager is None:
344
- _ipc_auth_manager = IPCAuthManager()
345
- return _ipc_auth_manager
346
-
347
-
348
- def require_ipc_token(scope: str):
349
- """
350
- Decorator to require IPC token authentication for a function.
351
-
352
- Usage:
353
- @require_ipc_token("tool_call")
354
- def my_sensitive_function(token_id: str, ...):
355
- ...
356
- """
357
- def decorator(func):
358
- def wrapper(*args, **kwargs):
359
- token_id = kwargs.get("token_id") or (args[0] if args else None)
360
- if not token_id:
361
- raise PermissionError("IPC token required but not provided")
362
-
363
- manager = get_ipc_auth_manager()
364
- is_valid, error = manager.validate_token(token_id, scope)
365
-
366
- if not is_valid:
367
- raise PermissionError(f"IPC token validation failed: {error}")
368
-
369
- # Token is valid, proceed with function
370
- result = func(*args, **kwargs)
371
-
372
- # Consume token after successful execution
373
- manager.consume_token(token_id, resource_type=scope)
374
-
375
- return result
376
- return wrapper
377
- return decorator
@@ -1,129 +0,0 @@
1
- """
2
- LifecycleManager: The Cardiologist.
3
- Tracks the pulse of every agent and enforces the "Tombstone Protocol".
4
-
5
- Strategic Role:
6
- - MONITOR: Who is alive? Who is dead?
7
- - CONTROL: Kill switch functionality.
8
- - SECURITY: Once Tombstoned, NEVER Resurrect.
9
- """
10
-
11
- import json
12
- import logging
13
- from enum import Enum
14
- from pathlib import Path
15
- from datetime import datetime, timedelta
16
- from typing import Dict, Any
17
- from .locking import get_lock
18
-
19
- logger = logging.getLogger("LIFECYCLE")
20
-
21
- class AgentState(str, Enum):
22
- ACTIVE = "active"
23
- STOPPED = "stopped"
24
- CRASHED = "crashed"
25
- TOMBSTONED = "tombstoned" # Permanently banned/killed
26
-
27
- class LifecycleManager:
28
- def __init__(self, brain_path: Path):
29
- self.brain_path = brain_path
30
- self.ledger_path = brain_path / "ledger" / "lifecycle.json"
31
-
32
- # Ensure ledger dir exists
33
- self.ledger_path.parent.mkdir(parents=True, exist_ok=True)
34
-
35
- def _load_ledger(self) -> Dict[str, Dict[str, Any]]:
36
- with get_lock("lifecycle", self.brain_path).section():
37
- if self.ledger_path.exists():
38
- try:
39
- return json.loads(self.ledger_path.read_text())
40
- except Exception:
41
- pass
42
- return {}
43
-
44
- def _save_ledger(self, ledger: Dict):
45
- with get_lock("lifecycle", self.brain_path).section():
46
- self.ledger_path.write_text(json.dumps(ledger, indent=2))
47
-
48
- def register_agent(self, agent_id: str):
49
- """Register a new agent instance as ACTIVE."""
50
- ledger = self._load_ledger()
51
-
52
- # Check if already tombstoned
53
- if agent_id in ledger:
54
- if ledger[agent_id]["state"] == AgentState.TOMBSTONED:
55
- raise PermissionError(f"Cannot register Tombstoned agent: {agent_id}")
56
-
57
- ledger[agent_id] = {
58
- "state": AgentState.ACTIVE.value,
59
- "last_heartbeat": datetime.now().isoformat(),
60
- "reason": "Registered"
61
- }
62
- self._save_ledger(ledger)
63
- logger.info(f"❤️ Agent Registered: {agent_id}")
64
-
65
- def update_state(self, agent_id: str, new_state: AgentState, reason: str = ""):
66
- """Transition agent state."""
67
- ledger = self._load_ledger()
68
-
69
- if agent_id not in ledger:
70
- logger.warning(f"Attempted to update unknown agent: {agent_id}")
71
- return
72
-
73
- current_state = ledger[agent_id]["state"]
74
-
75
- # PROTOCOL: TOMBSTONE IS FINAL
76
- if current_state == AgentState.TOMBSTONED:
77
- if new_state != AgentState.TOMBSTONED:
78
- logger.error(f"🚫 BLOCKED RESURRECTION of {agent_id}")
79
- raise PermissionError(f"TOMBSTONE PROTOCOL: {agent_id} cannot be revived.")
80
- return # No-op if setting to tombstone again
81
-
82
- ledger[agent_id]["state"] = new_state.value
83
- ledger[agent_id]["reason"] = reason
84
- ledger[agent_id]["last_heartbeat"] = datetime.now().isoformat()
85
-
86
- self._save_ledger(ledger)
87
- logger.info(f"🔄 State Change: {agent_id} -> {new_state} ({reason})")
88
-
89
- def record_heartbeat(self, agent_id: str):
90
- """Update last_heartbeat timestamp."""
91
- ledger = self._load_ledger()
92
- if agent_id in ledger and ledger[agent_id]["state"] == AgentState.ACTIVE:
93
- ledger[agent_id]["last_heartbeat"] = datetime.now().isoformat()
94
- self._save_ledger(ledger)
95
-
96
- def tombstone_agent(self, agent_id: str, reason: str):
97
- """Permanently disable an agent."""
98
- self.update_state(agent_id, AgentState.TOMBSTONED, reason)
99
-
100
- def get_state(self, agent_id: str) -> AgentState:
101
- ledger = self._load_ledger()
102
- if agent_id not in ledger:
103
- return AgentState.STOPPED # Default assumption if unknown
104
- return AgentState(ledger[agent_id]["state"])
105
-
106
- def can_execute(self, agent_id: str) -> bool:
107
- """Check if agent is allowed to execute commands."""
108
- state = self.get_state(agent_id)
109
- if state == AgentState.TOMBSTONED:
110
- return False
111
- # We might allow STOPPED to restart, or CRASHED to restart.
112
- # But TOMBSTONED is hard no.
113
- return True
114
-
115
- def is_alive(self, agent_id: str, timeout_seconds: int = 60) -> bool:
116
- """Check if agent is Active and recent heartbeat."""
117
- ledger = self._load_ledger()
118
- if agent_id not in ledger:
119
- return False
120
-
121
- data = ledger[agent_id]
122
- if AgentState(data["state"]) != AgentState.ACTIVE:
123
- return False
124
-
125
- last_hb = datetime.fromisoformat(data["last_heartbeat"])
126
- if datetime.now() - last_hb > timedelta(seconds=timeout_seconds):
127
- return False
128
-
129
- return True