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