nexo-brain 7.30.21 → 7.30.23
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/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/bin/nexo-brain.js +7 -1
- package/bin/nexo-managed-mcp.js +42 -4
- package/package.json +1 -1
- package/src/auto_update.py +7 -0
- package/src/cli.py +69 -0
- package/src/closure_plane.py +389 -8
- package/src/db/_schema.py +205 -2
- package/src/managed_mcp/catalog.py +6 -0
- package/src/managed_mcp/reconcile.py +198 -0
- package/src/opportunity_orchestrator.py +933 -0
- package/src/product_knowledge/catalog.json +53 -3
- package/src/scripts/nexo-email-monitor.py +71 -5
- package/src/scripts/nexo-send-reply.py +15 -2
- package/src/server.py +122 -2
- package/templates/core-prompts/email-monitor.md +6 -1
- package/tool-enforcement-map.json +120 -0
package/src/db/_schema.py
CHANGED
|
@@ -2867,6 +2867,202 @@ def _m78_operational_closure_plane(conn):
|
|
|
2867
2867
|
_migrate_add_index(conn, "idx_closure_events_item", "closure_item_events", "closure_item_id, created_at")
|
|
2868
2868
|
|
|
2869
2869
|
|
|
2870
|
+
def _m79_operational_closure_links_readiness(conn):
|
|
2871
|
+
"""Upgrade Closure Plane with links and capability readiness tables."""
|
|
2872
|
+
_m78_operational_closure_plane(conn)
|
|
2873
|
+
conn.execute(
|
|
2874
|
+
"""
|
|
2875
|
+
CREATE TABLE IF NOT EXISTS closure_item_links (
|
|
2876
|
+
id TEXT PRIMARY KEY,
|
|
2877
|
+
closure_item_id TEXT NOT NULL,
|
|
2878
|
+
link_type TEXT NOT NULL,
|
|
2879
|
+
link_id TEXT NOT NULL,
|
|
2880
|
+
relation TEXT NOT NULL DEFAULT 'related',
|
|
2881
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2882
|
+
FOREIGN KEY(closure_item_id) REFERENCES closure_items(id) ON DELETE CASCADE,
|
|
2883
|
+
UNIQUE(closure_item_id, link_type, link_id, relation)
|
|
2884
|
+
)
|
|
2885
|
+
"""
|
|
2886
|
+
)
|
|
2887
|
+
conn.execute(
|
|
2888
|
+
"""
|
|
2889
|
+
CREATE TABLE IF NOT EXISTS closure_capability_readiness (
|
|
2890
|
+
id TEXT PRIMARY KEY,
|
|
2891
|
+
capability TEXT NOT NULL,
|
|
2892
|
+
status TEXT NOT NULL DEFAULT 'unknown',
|
|
2893
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
2894
|
+
verified_at TEXT NOT NULL,
|
|
2895
|
+
verification_evidence TEXT NOT NULL DEFAULT '',
|
|
2896
|
+
expires_at TEXT NOT NULL DEFAULT '',
|
|
2897
|
+
UNIQUE(capability)
|
|
2898
|
+
)
|
|
2899
|
+
"""
|
|
2900
|
+
)
|
|
2901
|
+
_migrate_add_index(conn, "idx_closure_links_item", "closure_item_links", "closure_item_id")
|
|
2902
|
+
_migrate_add_index(conn, "idx_closure_links_target", "closure_item_links", "link_type, link_id")
|
|
2903
|
+
_migrate_add_index(conn, "idx_closure_readiness_capability", "closure_capability_readiness", "capability, status")
|
|
2904
|
+
|
|
2905
|
+
|
|
2906
|
+
def _m80_opportunity_orchestrator(conn):
|
|
2907
|
+
"""Opportunity Orchestrator v1: sparse, evidence-backed proactive queue."""
|
|
2908
|
+
conn.execute(
|
|
2909
|
+
"""
|
|
2910
|
+
CREATE TABLE IF NOT EXISTS nexo_signals (
|
|
2911
|
+
id TEXT PRIMARY KEY,
|
|
2912
|
+
source_type TEXT NOT NULL,
|
|
2913
|
+
source_id TEXT NOT NULL DEFAULT '',
|
|
2914
|
+
entity_ref TEXT NOT NULL DEFAULT '',
|
|
2915
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
2916
|
+
signal_kind TEXT NOT NULL,
|
|
2917
|
+
urgency REAL NOT NULL DEFAULT 0,
|
|
2918
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
2919
|
+
privacy_level TEXT NOT NULL DEFAULT 'normal',
|
|
2920
|
+
source_hash TEXT NOT NULL,
|
|
2921
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2922
|
+
expires_at TEXT NOT NULL DEFAULT '',
|
|
2923
|
+
UNIQUE(source_type, source_id, signal_kind)
|
|
2924
|
+
)
|
|
2925
|
+
"""
|
|
2926
|
+
)
|
|
2927
|
+
conn.execute(
|
|
2928
|
+
"""
|
|
2929
|
+
CREATE TABLE IF NOT EXISTS nexo_opportunities (
|
|
2930
|
+
id TEXT PRIMARY KEY,
|
|
2931
|
+
title TEXT NOT NULL,
|
|
2932
|
+
hypothesis TEXT NOT NULL DEFAULT '',
|
|
2933
|
+
domain TEXT NOT NULL DEFAULT 'general',
|
|
2934
|
+
opportunity_type TEXT NOT NULL,
|
|
2935
|
+
dedupe_key TEXT NOT NULL,
|
|
2936
|
+
impact REAL NOT NULL DEFAULT 0,
|
|
2937
|
+
urgency REAL NOT NULL DEFAULT 0,
|
|
2938
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
2939
|
+
risk REAL NOT NULL DEFAULT 0,
|
|
2940
|
+
effort REAL NOT NULL DEFAULT 0,
|
|
2941
|
+
readiness REAL NOT NULL DEFAULT 0,
|
|
2942
|
+
user_burden_reduction REAL NOT NULL DEFAULT 0,
|
|
2943
|
+
interruption_cost REAL NOT NULL DEFAULT 0,
|
|
2944
|
+
strategic_alignment REAL NOT NULL DEFAULT 0,
|
|
2945
|
+
repetition_penalty REAL NOT NULL DEFAULT 0,
|
|
2946
|
+
score REAL NOT NULL DEFAULT 0,
|
|
2947
|
+
state TEXT NOT NULL DEFAULT 'candidate',
|
|
2948
|
+
owner TEXT NOT NULL DEFAULT 'nero',
|
|
2949
|
+
why_now TEXT NOT NULL DEFAULT '',
|
|
2950
|
+
next_action TEXT NOT NULL DEFAULT '',
|
|
2951
|
+
action_class TEXT NOT NULL DEFAULT 'read_only',
|
|
2952
|
+
authorization_status TEXT NOT NULL DEFAULT 'not_required',
|
|
2953
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2954
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2955
|
+
expires_at TEXT NOT NULL DEFAULT '',
|
|
2956
|
+
last_proposed_at TEXT NOT NULL DEFAULT '',
|
|
2957
|
+
source_payload_json TEXT NOT NULL DEFAULT '{}',
|
|
2958
|
+
UNIQUE(dedupe_key)
|
|
2959
|
+
)
|
|
2960
|
+
"""
|
|
2961
|
+
)
|
|
2962
|
+
conn.execute(
|
|
2963
|
+
"""
|
|
2964
|
+
CREATE TABLE IF NOT EXISTS nexo_opportunity_evidence (
|
|
2965
|
+
id TEXT PRIMARY KEY,
|
|
2966
|
+
opportunity_id TEXT NOT NULL,
|
|
2967
|
+
source_type TEXT NOT NULL,
|
|
2968
|
+
source_id TEXT NOT NULL,
|
|
2969
|
+
evidence_summary TEXT NOT NULL DEFAULT '',
|
|
2970
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
2971
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2972
|
+
FOREIGN KEY(opportunity_id) REFERENCES nexo_opportunities(id) ON DELETE CASCADE,
|
|
2973
|
+
UNIQUE(opportunity_id, source_type, source_id)
|
|
2974
|
+
)
|
|
2975
|
+
"""
|
|
2976
|
+
)
|
|
2977
|
+
conn.execute(
|
|
2978
|
+
"""
|
|
2979
|
+
CREATE TABLE IF NOT EXISTS nexo_preparations (
|
|
2980
|
+
id TEXT PRIMARY KEY,
|
|
2981
|
+
opportunity_id TEXT NOT NULL,
|
|
2982
|
+
artifact_type TEXT NOT NULL,
|
|
2983
|
+
artifact_ref TEXT NOT NULL DEFAULT '',
|
|
2984
|
+
safe_mode INTEGER NOT NULL DEFAULT 1,
|
|
2985
|
+
approval_required INTEGER NOT NULL DEFAULT 0,
|
|
2986
|
+
status TEXT NOT NULL DEFAULT 'ready',
|
|
2987
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2988
|
+
expires_at TEXT NOT NULL DEFAULT '',
|
|
2989
|
+
FOREIGN KEY(opportunity_id) REFERENCES nexo_opportunities(id) ON DELETE CASCADE,
|
|
2990
|
+
UNIQUE(opportunity_id, artifact_type, artifact_ref)
|
|
2991
|
+
)
|
|
2992
|
+
"""
|
|
2993
|
+
)
|
|
2994
|
+
conn.execute(
|
|
2995
|
+
"""
|
|
2996
|
+
CREATE TABLE IF NOT EXISTS nexo_proposals (
|
|
2997
|
+
id TEXT PRIMARY KEY,
|
|
2998
|
+
opportunity_id TEXT NOT NULL,
|
|
2999
|
+
surface TEXT NOT NULL DEFAULT 'home',
|
|
3000
|
+
copy TEXT NOT NULL DEFAULT '',
|
|
3001
|
+
cta_primary TEXT NOT NULL DEFAULT 'Inspect evidence',
|
|
3002
|
+
cta_secondary TEXT NOT NULL DEFAULT 'Snooze',
|
|
3003
|
+
shown_at TEXT NOT NULL DEFAULT '',
|
|
3004
|
+
feedback TEXT NOT NULL DEFAULT '',
|
|
3005
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3006
|
+
FOREIGN KEY(opportunity_id) REFERENCES nexo_opportunities(id) ON DELETE CASCADE,
|
|
3007
|
+
UNIQUE(opportunity_id, surface)
|
|
3008
|
+
)
|
|
3009
|
+
"""
|
|
3010
|
+
)
|
|
3011
|
+
conn.execute(
|
|
3012
|
+
"""
|
|
3013
|
+
CREATE TABLE IF NOT EXISTS nexo_proposal_events (
|
|
3014
|
+
id TEXT PRIMARY KEY,
|
|
3015
|
+
proposal_id TEXT NOT NULL,
|
|
3016
|
+
event_type TEXT NOT NULL,
|
|
3017
|
+
feedback TEXT NOT NULL DEFAULT '',
|
|
3018
|
+
note TEXT NOT NULL DEFAULT '',
|
|
3019
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
3020
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3021
|
+
FOREIGN KEY(proposal_id) REFERENCES nexo_proposals(id) ON DELETE CASCADE
|
|
3022
|
+
)
|
|
3023
|
+
"""
|
|
3024
|
+
)
|
|
3025
|
+
conn.execute(
|
|
3026
|
+
"""
|
|
3027
|
+
CREATE TABLE IF NOT EXISTS nexo_suppression_rules (
|
|
3028
|
+
id TEXT PRIMARY KEY,
|
|
3029
|
+
scope_type TEXT NOT NULL,
|
|
3030
|
+
scope_key TEXT NOT NULL,
|
|
3031
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
3032
|
+
expires_at TEXT NOT NULL DEFAULT '',
|
|
3033
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3034
|
+
UNIQUE(scope_type, scope_key, reason)
|
|
3035
|
+
)
|
|
3036
|
+
"""
|
|
3037
|
+
)
|
|
3038
|
+
conn.execute(
|
|
3039
|
+
"""
|
|
3040
|
+
CREATE TABLE IF NOT EXISTS nexo_action_authorizations (
|
|
3041
|
+
id TEXT PRIMARY KEY,
|
|
3042
|
+
scope TEXT NOT NULL,
|
|
3043
|
+
allowed_action_class TEXT NOT NULL,
|
|
3044
|
+
max_cost REAL NOT NULL DEFAULT 0,
|
|
3045
|
+
expires_at TEXT NOT NULL DEFAULT '',
|
|
3046
|
+
granted_by TEXT NOT NULL DEFAULT '',
|
|
3047
|
+
evidence_ref TEXT NOT NULL DEFAULT '',
|
|
3048
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3049
|
+
UNIQUE(scope, allowed_action_class, evidence_ref)
|
|
3050
|
+
)
|
|
3051
|
+
"""
|
|
3052
|
+
)
|
|
3053
|
+
_migrate_add_index(conn, "idx_nexo_signals_source", "nexo_signals", "source_type, source_id")
|
|
3054
|
+
_migrate_add_index(conn, "idx_nexo_signals_expires", "nexo_signals", "expires_at")
|
|
3055
|
+
_migrate_add_index(conn, "idx_nexo_opportunities_state_score", "nexo_opportunities", "state, score DESC, updated_at")
|
|
3056
|
+
_migrate_add_index(conn, "idx_nexo_opportunities_type", "nexo_opportunities", "opportunity_type, state")
|
|
3057
|
+
_migrate_add_index(conn, "idx_nexo_opportunities_expires", "nexo_opportunities", "expires_at")
|
|
3058
|
+
_migrate_add_index(conn, "idx_nexo_opportunity_evidence_item", "nexo_opportunity_evidence", "opportunity_id")
|
|
3059
|
+
_migrate_add_index(conn, "idx_nexo_preparations_item", "nexo_preparations", "opportunity_id, status")
|
|
3060
|
+
_migrate_add_index(conn, "idx_nexo_proposals_surface", "nexo_proposals", "surface, feedback")
|
|
3061
|
+
_migrate_add_index(conn, "idx_nexo_proposal_events_proposal", "nexo_proposal_events", "proposal_id, created_at")
|
|
3062
|
+
_migrate_add_index(conn, "idx_nexo_suppression_scope", "nexo_suppression_rules", "scope_type, scope_key")
|
|
3063
|
+
_migrate_add_index(conn, "idx_nexo_authorizations_scope", "nexo_action_authorizations", "scope, allowed_action_class")
|
|
3064
|
+
|
|
3065
|
+
|
|
2870
3066
|
MIGRATIONS = [
|
|
2871
3067
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
2872
3068
|
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
@@ -2946,6 +3142,8 @@ MIGRATIONS = [
|
|
|
2946
3142
|
(76, "semantic_layers", _m76_semantic_layers),
|
|
2947
3143
|
(77, "morning_briefing_presentation", _m77_morning_briefing_presentation),
|
|
2948
3144
|
(78, "operational_closure_plane", _m78_operational_closure_plane),
|
|
3145
|
+
(79, "operational_closure_links_readiness", _m79_operational_closure_links_readiness),
|
|
3146
|
+
(80, "opportunity_orchestrator", _m80_opportunity_orchestrator),
|
|
2949
3147
|
]
|
|
2950
3148
|
|
|
2951
3149
|
|
|
@@ -2967,7 +3165,11 @@ def run_migrations(conn=None):
|
|
|
2967
3165
|
""")
|
|
2968
3166
|
conn.commit()
|
|
2969
3167
|
|
|
2970
|
-
applied = {
|
|
3168
|
+
applied = {
|
|
3169
|
+
int(r[0])
|
|
3170
|
+
for r in conn.execute("SELECT version FROM schema_migrations").fetchall()
|
|
3171
|
+
if str(r[0]).strip().isdigit()
|
|
3172
|
+
}
|
|
2971
3173
|
|
|
2972
3174
|
failed = []
|
|
2973
3175
|
for version, name, fn in MIGRATIONS:
|
|
@@ -2975,9 +3177,10 @@ def run_migrations(conn=None):
|
|
|
2975
3177
|
try:
|
|
2976
3178
|
fn(conn)
|
|
2977
3179
|
conn.execute(
|
|
2978
|
-
"INSERT INTO schema_migrations (version, name) VALUES (?, ?)",
|
|
3180
|
+
"INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (?, ?)",
|
|
2979
3181
|
(version, name)
|
|
2980
3182
|
)
|
|
3183
|
+
applied.add(version)
|
|
2981
3184
|
conn.commit()
|
|
2982
3185
|
except Exception as e:
|
|
2983
3186
|
conn.rollback()
|
|
@@ -189,6 +189,12 @@ def provider_for_capability(
|
|
|
189
189
|
|
|
190
190
|
|
|
191
191
|
def _runner_path(nexo_home: Path, runtime_root: Path | None = None) -> Path:
|
|
192
|
+
runtime_bin = nexo_home / "bin" / "nexo-managed-mcp"
|
|
193
|
+
if runtime_bin.exists():
|
|
194
|
+
return runtime_bin
|
|
195
|
+
runtime_js = nexo_home / "bin" / "nexo-managed-mcp.js"
|
|
196
|
+
if runtime_js.exists():
|
|
197
|
+
return runtime_js
|
|
192
198
|
runtime_bin = nexo_home / "runtime" / "bin" / "nexo-managed-mcp"
|
|
193
199
|
if runtime_bin.exists():
|
|
194
200
|
return runtime_bin
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
5
7
|
import time
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
from typing import Any
|
|
@@ -17,6 +19,23 @@ def _state_path(nexo_home: Path) -> Path:
|
|
|
17
19
|
return _state_dir(nexo_home) / "installed-state.json"
|
|
18
20
|
|
|
19
21
|
|
|
22
|
+
def _artifacts_dir(nexo_home: Path) -> Path:
|
|
23
|
+
return _state_dir(nexo_home) / "artifacts"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _provider_root(nexo_home: Path, provider_id: str) -> Path:
|
|
27
|
+
return _artifacts_dir(nexo_home) / provider_id
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _provider_stage_dir(nexo_home: Path, provider_id: str, version: str) -> Path:
|
|
31
|
+
return _provider_root(nexo_home, provider_id) / version
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _provider_wrapper_path(nexo_home: Path, provider_id: str, *, platform: str | None = None) -> Path:
|
|
35
|
+
suffix = ".cmd" if str(platform or os.name).lower().startswith(("win", "nt")) else ""
|
|
36
|
+
return _provider_root(nexo_home, provider_id) / "bin" / f"{provider_id}{suffix}"
|
|
37
|
+
|
|
38
|
+
|
|
20
39
|
def _read_state(nexo_home: Path) -> dict[str, Any]:
|
|
21
40
|
path = _state_path(nexo_home)
|
|
22
41
|
if not path.is_file():
|
|
@@ -37,6 +56,160 @@ def _write_state(nexo_home: Path, state: dict[str, Any]) -> None:
|
|
|
37
56
|
tmp.replace(path)
|
|
38
57
|
|
|
39
58
|
|
|
59
|
+
def _provider_ids_from_desired(desired: dict[str, Any]) -> set[str]:
|
|
60
|
+
provider_ids: set[str] = set()
|
|
61
|
+
for client_entries in desired.values():
|
|
62
|
+
if not isinstance(client_entries, dict):
|
|
63
|
+
continue
|
|
64
|
+
for entry in client_entries.values():
|
|
65
|
+
meta = entry.get("nexo") if isinstance(entry, dict) else {}
|
|
66
|
+
if isinstance(meta, dict) and meta.get("provider_id"):
|
|
67
|
+
provider_ids.add(str(meta["provider_id"]))
|
|
68
|
+
return provider_ids
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _locked_providers(lock: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
72
|
+
providers = lock.get("providers") if isinstance(lock.get("providers"), dict) else {}
|
|
73
|
+
return {str(key): value for key, value in providers.items() if isinstance(value, dict)}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _run_npm_install(stage_dir: Path, package: str, version: str) -> subprocess.CompletedProcess:
|
|
77
|
+
npm = "npm.cmd" if os.name == "nt" else "npm"
|
|
78
|
+
return subprocess.run(
|
|
79
|
+
[
|
|
80
|
+
npm,
|
|
81
|
+
"install",
|
|
82
|
+
"--prefix",
|
|
83
|
+
str(stage_dir),
|
|
84
|
+
"--omit=dev",
|
|
85
|
+
"--no-audit",
|
|
86
|
+
"--no-fund",
|
|
87
|
+
"--package-lock=false",
|
|
88
|
+
f"{package}@{version}",
|
|
89
|
+
],
|
|
90
|
+
text=True,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
timeout=180,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _write_provider_wrappers(
|
|
97
|
+
*,
|
|
98
|
+
nexo_home: Path,
|
|
99
|
+
provider_id: str,
|
|
100
|
+
version: str,
|
|
101
|
+
provider_bin: str,
|
|
102
|
+
) -> dict[str, str]:
|
|
103
|
+
root = _provider_root(nexo_home, provider_id)
|
|
104
|
+
bin_dir = root / "bin"
|
|
105
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
stage_dir = _provider_stage_dir(nexo_home, provider_id, version)
|
|
107
|
+
unix_target = stage_dir / "node_modules" / ".bin" / provider_bin
|
|
108
|
+
win_target = stage_dir / "node_modules" / ".bin" / f"{provider_bin}.cmd"
|
|
109
|
+
unix_wrapper = bin_dir / provider_id
|
|
110
|
+
unix_wrapper.write_text(f"#!/bin/sh\nexec {json.dumps(str(unix_target))} \"$@\"\n")
|
|
111
|
+
unix_wrapper.chmod(0o755)
|
|
112
|
+
win_wrapper = bin_dir / f"{provider_id}.cmd"
|
|
113
|
+
win_wrapper.write_text(f"@echo off\r\ncall \"{win_target}\" %*\r\n")
|
|
114
|
+
return {
|
|
115
|
+
"unix": str(unix_wrapper),
|
|
116
|
+
"win32": str(win_wrapper),
|
|
117
|
+
"target": str(unix_target),
|
|
118
|
+
"target_win32": str(win_target),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _stage_provider(
|
|
123
|
+
*,
|
|
124
|
+
nexo_home: Path,
|
|
125
|
+
provider_id: str,
|
|
126
|
+
locked: dict[str, Any],
|
|
127
|
+
npm_runner=_run_npm_install,
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
package = str(locked.get("package") or "").strip()
|
|
130
|
+
version = str(locked.get("version") or "").strip()
|
|
131
|
+
provider_bin = str(locked.get("bin") or "").strip()
|
|
132
|
+
if not package or not version or not provider_bin:
|
|
133
|
+
return {"status": "failed", "error": "provider lock is incomplete"}
|
|
134
|
+
stage_dir = _provider_stage_dir(nexo_home, provider_id, version)
|
|
135
|
+
root = _provider_root(nexo_home, provider_id)
|
|
136
|
+
tmp_dir = root / f".stage-{version}-{int(time.time() * 1000)}"
|
|
137
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
if stage_dir.is_dir():
|
|
139
|
+
wrappers = _write_provider_wrappers(
|
|
140
|
+
nexo_home=nexo_home,
|
|
141
|
+
provider_id=provider_id,
|
|
142
|
+
version=version,
|
|
143
|
+
provider_bin=provider_bin,
|
|
144
|
+
)
|
|
145
|
+
return {
|
|
146
|
+
"status": "healthy",
|
|
147
|
+
"version": version,
|
|
148
|
+
"package": package,
|
|
149
|
+
"staged_path": str(stage_dir),
|
|
150
|
+
"executable": wrappers["unix"],
|
|
151
|
+
"executable_win32": wrappers["win32"],
|
|
152
|
+
"reused": True,
|
|
153
|
+
}
|
|
154
|
+
try:
|
|
155
|
+
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
result = npm_runner(tmp_dir, package, version)
|
|
157
|
+
if getattr(result, "returncode", 1) != 0:
|
|
158
|
+
return {
|
|
159
|
+
"status": "failed",
|
|
160
|
+
"version": version,
|
|
161
|
+
"package": package,
|
|
162
|
+
"error": (getattr(result, "stderr", "") or getattr(result, "stdout", "") or "npm install failed")[-1200:],
|
|
163
|
+
}
|
|
164
|
+
if stage_dir.exists():
|
|
165
|
+
shutil.rmtree(stage_dir)
|
|
166
|
+
tmp_dir.replace(stage_dir)
|
|
167
|
+
wrappers = _write_provider_wrappers(
|
|
168
|
+
nexo_home=nexo_home,
|
|
169
|
+
provider_id=provider_id,
|
|
170
|
+
version=version,
|
|
171
|
+
provider_bin=provider_bin,
|
|
172
|
+
)
|
|
173
|
+
return {
|
|
174
|
+
"status": "healthy",
|
|
175
|
+
"version": version,
|
|
176
|
+
"package": package,
|
|
177
|
+
"staged_path": str(stage_dir),
|
|
178
|
+
"executable": wrappers["unix"],
|
|
179
|
+
"executable_win32": wrappers["win32"],
|
|
180
|
+
"integrity": str(locked.get("integrity") or ""),
|
|
181
|
+
}
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
return {"status": "failed", "version": version, "package": package, "error": str(exc)}
|
|
184
|
+
finally:
|
|
185
|
+
if tmp_dir.exists():
|
|
186
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _provider_health(
|
|
190
|
+
*,
|
|
191
|
+
nexo_home: Path,
|
|
192
|
+
provider_id: str,
|
|
193
|
+
locked: dict[str, Any],
|
|
194
|
+
platform: str | None = None,
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
version = str(locked.get("version") or "").strip()
|
|
197
|
+
executable = _provider_wrapper_path(nexo_home, provider_id, platform=platform)
|
|
198
|
+
stage_dir = _provider_stage_dir(nexo_home, provider_id, version) if version else _provider_root(nexo_home, provider_id)
|
|
199
|
+
if not version:
|
|
200
|
+
return {"status": "failed", "reason": "missing_version"}
|
|
201
|
+
if not stage_dir.is_dir():
|
|
202
|
+
return {"status": "unstaged", "version": version, "staged_path": str(stage_dir)}
|
|
203
|
+
if not executable.exists():
|
|
204
|
+
return {"status": "failed", "version": version, "reason": "wrapper_missing", "executable": str(executable)}
|
|
205
|
+
return {
|
|
206
|
+
"status": "healthy",
|
|
207
|
+
"version": version,
|
|
208
|
+
"staged_path": str(stage_dir),
|
|
209
|
+
"executable": str(executable),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
40
213
|
def reconcile_managed_mcp(
|
|
41
214
|
*,
|
|
42
215
|
nexo_home: str | os.PathLike[str] | Path,
|
|
@@ -44,6 +217,7 @@ def reconcile_managed_mcp(
|
|
|
44
217
|
clients: list[str] | tuple[str, ...] | None = None,
|
|
45
218
|
apply: bool = False,
|
|
46
219
|
platform: str | None = None,
|
|
220
|
+
npm_runner=_run_npm_install,
|
|
47
221
|
) -> dict[str, Any]:
|
|
48
222
|
nexo_home_path = Path(nexo_home).expanduser()
|
|
49
223
|
catalog = load_catalog()
|
|
@@ -76,6 +250,27 @@ def reconcile_managed_mcp(
|
|
|
76
250
|
actions.append({"client": client, "server": name, "action": action})
|
|
77
251
|
for name in set(old_entries) - set(entries):
|
|
78
252
|
actions.append({"client": client, "server": name, "action": "disable"})
|
|
253
|
+
locked_by_provider = _locked_providers(lock)
|
|
254
|
+
provider_ids = _provider_ids_from_desired(desired)
|
|
255
|
+
provider_state: dict[str, Any] = {}
|
|
256
|
+
if apply:
|
|
257
|
+
for provider_id in sorted(provider_ids):
|
|
258
|
+
provider_state[provider_id] = _stage_provider(
|
|
259
|
+
nexo_home=nexo_home_path,
|
|
260
|
+
provider_id=provider_id,
|
|
261
|
+
locked=locked_by_provider.get(provider_id) or {},
|
|
262
|
+
npm_runner=npm_runner,
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
for provider_id in sorted(provider_ids):
|
|
266
|
+
provider_state[provider_id] = _provider_health(
|
|
267
|
+
nexo_home=nexo_home_path,
|
|
268
|
+
provider_id=provider_id,
|
|
269
|
+
locked=locked_by_provider.get(provider_id) or {},
|
|
270
|
+
platform=platform,
|
|
271
|
+
)
|
|
272
|
+
if any((state.get("status") if isinstance(state, dict) else "") not in {"healthy"} for state in provider_state.values()):
|
|
273
|
+
actions.append({"client": "runtime", "server": "managed_mcp", "action": "healthcheck"})
|
|
79
274
|
|
|
80
275
|
state = {
|
|
81
276
|
"schema": "nexo.managed_mcp.state.v1",
|
|
@@ -83,6 +278,7 @@ def reconcile_managed_mcp(
|
|
|
83
278
|
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
84
279
|
"validation": validation,
|
|
85
280
|
"desired": desired,
|
|
281
|
+
"providers": provider_state,
|
|
86
282
|
"last_plan": actions,
|
|
87
283
|
}
|
|
88
284
|
if apply:
|
|
@@ -94,6 +290,7 @@ def reconcile_managed_mcp(
|
|
|
94
290
|
"validation": validation,
|
|
95
291
|
"actions": actions,
|
|
96
292
|
"desired_clients": sorted(desired),
|
|
293
|
+
"providers": provider_state,
|
|
97
294
|
}
|
|
98
295
|
|
|
99
296
|
|
|
@@ -119,4 +316,5 @@ def managed_mcp_status(
|
|
|
119
316
|
"validation": plan["validation"],
|
|
120
317
|
"last_applied_at": state.get("updated_at", ""),
|
|
121
318
|
"actions": plan["actions"],
|
|
319
|
+
"providers": plan.get("providers", {}),
|
|
122
320
|
}
|