nexo-brain 7.30.22 → 7.30.24
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 +5 -1
- package/package.json +1 -1
- package/src/client_sync.py +57 -4
- package/src/db/_schema.py +161 -0
- package/src/managed_mcp/__init__.py +8 -1
- package/src/managed_mcp/client_config.py +41 -0
- package/src/opportunity_orchestrator.py +933 -0
- package/src/scripts/nexo-email-monitor.py +71 -5
- package/src/scripts/nexo-send-reply.py +15 -2
- package/src/server.py +58 -0
- package/templates/core-prompts/email-monitor.md +6 -1
- package/tool-enforcement-map.json +75 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.24",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.30.
|
|
21
|
+
Version `7.30.24` is the current packaged-runtime line. Patch release over v7.30.23 - client sync now stages and verifies managed MCP providers before writing Brain-owned default MCP entries into Claude, Codex, or Desktop configs.
|
|
22
|
+
|
|
23
|
+
Previously in `7.30.23`: patch release over v7.30.22 - Brain ships Opportunity Orchestrator Phase 1 for evidence-backed proactive preparation, read-only dry-run reports, bounded proposal queues, and feedback/suppression controls.
|
|
22
24
|
|
|
23
25
|
Previously in `7.30.20`: patch release over v7.30.19 - packaged installs now copy the `product_knowledge` package into the installed runtime so `nexo update` can import the new product knowledge tools.
|
|
24
26
|
|
|
@@ -26,6 +28,8 @@ Previously in `7.30.19`: patch release over v7.30.18 - product capabilities now
|
|
|
26
28
|
|
|
27
29
|
Previously in `7.30.16`: patch release over v7.30.14 - Desktop diagnostics can read embedding migration status without warming models, and the coordinated Desktop update path is covered for bundled model verification and obsolete managed model cleanup.
|
|
28
30
|
|
|
31
|
+
Previously in `7.30.15`: patch release over v7.30.14 - Email NEXO closes affirmative actionable replies correctly and avoids reopening processed threads after a sent reply is already recorded.
|
|
32
|
+
|
|
29
33
|
Previously in `7.30.14`: patch release over v7.30.13 - support tickets, provider capability discovery, task-close rearming, internal audit followups, and the memory-observation watchdog are aligned for Desktop-managed agents.
|
|
30
34
|
|
|
31
35
|
Previously in `7.30.13`: patch release over v7.30.12 - Email NEXO monitor prompts now require a full IMAP body fetch before replying, and managed email defaults use the MXroute `Sent` folder instead of `INBOX.Sent`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.24",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/client_sync.py
CHANGED
|
@@ -24,11 +24,21 @@ from bootstrap_docs import sync_client_bootstrap
|
|
|
24
24
|
from runtime_home import resolve_nexo_home
|
|
25
25
|
|
|
26
26
|
try:
|
|
27
|
-
from managed_mcp import
|
|
27
|
+
from managed_mcp import (
|
|
28
|
+
build_managed_server_entries,
|
|
29
|
+
merge_json_mcp_servers,
|
|
30
|
+
merge_toml_mcp_servers,
|
|
31
|
+
reconcile_managed_mcp,
|
|
32
|
+
remove_json_nexo_managed_mcp_servers,
|
|
33
|
+
remove_toml_nexo_managed_mcp_servers,
|
|
34
|
+
)
|
|
28
35
|
except Exception:
|
|
29
36
|
build_managed_server_entries = None
|
|
30
37
|
merge_json_mcp_servers = None
|
|
31
38
|
merge_toml_mcp_servers = None
|
|
39
|
+
reconcile_managed_mcp = None
|
|
40
|
+
remove_json_nexo_managed_mcp_servers = None
|
|
41
|
+
remove_toml_nexo_managed_mcp_servers = None
|
|
32
42
|
|
|
33
43
|
try:
|
|
34
44
|
from client_preferences import (
|
|
@@ -842,7 +852,7 @@ def build_server_config(
|
|
|
842
852
|
def _managed_mcp_entries_for(client: str, server_config: dict) -> dict:
|
|
843
853
|
if str(os.environ.get("NEXO_MANAGED_MCP_DISABLE", "")).strip().lower() in {"1", "true", "yes", "on"}:
|
|
844
854
|
return {}
|
|
845
|
-
if not build_managed_server_entries:
|
|
855
|
+
if not build_managed_server_entries or not reconcile_managed_mcp:
|
|
846
856
|
return {}
|
|
847
857
|
env = server_config.get("env") if isinstance(server_config.get("env"), dict) else {}
|
|
848
858
|
nexo_home = str(env.get("NEXO_HOME") or "").strip()
|
|
@@ -855,16 +865,51 @@ def _managed_mcp_entries_for(client: str, server_config: dict) -> dict:
|
|
|
855
865
|
or ""
|
|
856
866
|
).strip()
|
|
857
867
|
try:
|
|
858
|
-
|
|
868
|
+
plan = reconcile_managed_mcp(
|
|
869
|
+
nexo_home=nexo_home,
|
|
870
|
+
runtime_root=runtime_root or None,
|
|
871
|
+
clients=[client],
|
|
872
|
+
apply=True,
|
|
873
|
+
platform=target_platform or None,
|
|
874
|
+
)
|
|
875
|
+
providers = plan.get("providers") if isinstance(plan.get("providers"), dict) else {}
|
|
876
|
+
if not plan.get("ok") or not providers:
|
|
877
|
+
return {}
|
|
878
|
+
entries = build_managed_server_entries(
|
|
859
879
|
client=client,
|
|
860
880
|
nexo_home=nexo_home,
|
|
861
881
|
runtime_root=runtime_root or None,
|
|
862
882
|
platform=target_platform or None,
|
|
863
883
|
)
|
|
884
|
+
required_providers = {
|
|
885
|
+
str((entry.get("nexo") or {}).get("provider_id") or "").strip()
|
|
886
|
+
for entry in entries.values()
|
|
887
|
+
if isinstance(entry, dict)
|
|
888
|
+
}
|
|
889
|
+
required_providers.discard("")
|
|
890
|
+
if not required_providers:
|
|
891
|
+
return {}
|
|
892
|
+
for provider_id in required_providers:
|
|
893
|
+
state = providers.get(provider_id) if isinstance(providers.get(provider_id), dict) else {}
|
|
894
|
+
if state.get("status") != "healthy":
|
|
895
|
+
return {}
|
|
896
|
+
return entries
|
|
864
897
|
except Exception:
|
|
865
898
|
return {}
|
|
866
899
|
|
|
867
900
|
|
|
901
|
+
def _remove_unhealthy_json_managed_mcp(payload: dict) -> dict:
|
|
902
|
+
if not remove_json_nexo_managed_mcp_servers:
|
|
903
|
+
return payload
|
|
904
|
+
return remove_json_nexo_managed_mcp_servers(payload)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _remove_unhealthy_toml_managed_mcp(payload: dict) -> dict:
|
|
908
|
+
if not remove_toml_nexo_managed_mcp_servers:
|
|
909
|
+
return payload
|
|
910
|
+
return remove_toml_nexo_managed_mcp_servers(payload)
|
|
911
|
+
|
|
912
|
+
|
|
868
913
|
def _claude_code_settings_path(home: Path | None = None) -> Path:
|
|
869
914
|
base = home or _user_home()
|
|
870
915
|
return base / ".claude" / "settings.json"
|
|
@@ -1039,6 +1084,10 @@ def _sync_codex_managed_config(
|
|
|
1039
1084
|
payload = merge_toml_mcp_servers(payload, managed_entries)
|
|
1040
1085
|
codex_table = payload.setdefault("nexo", {}).setdefault("codex", {})
|
|
1041
1086
|
codex_table["managed_default_mcp_count"] = len(managed_entries)
|
|
1087
|
+
else:
|
|
1088
|
+
payload = _remove_unhealthy_toml_managed_mcp(payload)
|
|
1089
|
+
codex_table = payload.setdefault("nexo", {}).setdefault("codex", {})
|
|
1090
|
+
codex_table["managed_default_mcp_count"] = 0
|
|
1042
1091
|
|
|
1043
1092
|
# Ensure Codex headless crons (followup-runner, email-monitor, deep-sleep,
|
|
1044
1093
|
# etc.) do not stall on approval prompts. Only set defaults when the user
|
|
@@ -1056,7 +1105,7 @@ def _sync_codex_managed_config(
|
|
|
1056
1105
|
"hooks_enabled": True,
|
|
1057
1106
|
"model": runtime_profile.get("model", ""),
|
|
1058
1107
|
"reasoning_effort": runtime_profile.get("reasoning_effort", "") or "",
|
|
1059
|
-
"managed_default_mcp_count": len(
|
|
1108
|
+
"managed_default_mcp_count": len(managed_entries) if server_config else 0,
|
|
1060
1109
|
}
|
|
1061
1110
|
|
|
1062
1111
|
|
|
@@ -1450,6 +1499,8 @@ def _sync_json_client(path: Path, server_config: dict, label: str, *, managed_me
|
|
|
1450
1499
|
managed_entries = _managed_mcp_entries_for(label, server_config)
|
|
1451
1500
|
if managed_entries and merge_json_mcp_servers:
|
|
1452
1501
|
payload = merge_json_mcp_servers(payload, managed_entries)
|
|
1502
|
+
else:
|
|
1503
|
+
payload = _remove_unhealthy_json_managed_mcp(payload)
|
|
1453
1504
|
_write_json_object(path, payload)
|
|
1454
1505
|
return {
|
|
1455
1506
|
"ok": True,
|
|
@@ -1537,6 +1588,8 @@ def _sync_claude_code_settings(path: Path, server_config: dict) -> dict:
|
|
|
1537
1588
|
managed_entries = _managed_mcp_entries_for("claude_code", server_config)
|
|
1538
1589
|
if managed_entries and merge_json_mcp_servers:
|
|
1539
1590
|
payload = merge_json_mcp_servers(payload, managed_entries)
|
|
1591
|
+
else:
|
|
1592
|
+
payload = _remove_unhealthy_json_managed_mcp(payload)
|
|
1540
1593
|
_ensure_headless_permissions(payload)
|
|
1541
1594
|
_write_json_object(path, payload)
|
|
1542
1595
|
return {
|
package/src/db/_schema.py
CHANGED
|
@@ -2903,6 +2903,166 @@ def _m79_operational_closure_links_readiness(conn):
|
|
|
2903
2903
|
_migrate_add_index(conn, "idx_closure_readiness_capability", "closure_capability_readiness", "capability, status")
|
|
2904
2904
|
|
|
2905
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
|
+
|
|
2906
3066
|
MIGRATIONS = [
|
|
2907
3067
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
2908
3068
|
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
@@ -2983,6 +3143,7 @@ MIGRATIONS = [
|
|
|
2983
3143
|
(77, "morning_briefing_presentation", _m77_morning_briefing_presentation),
|
|
2984
3144
|
(78, "operational_closure_plane", _m78_operational_closure_plane),
|
|
2985
3145
|
(79, "operational_closure_links_readiness", _m79_operational_closure_links_readiness),
|
|
3146
|
+
(80, "opportunity_orchestrator", _m80_opportunity_orchestrator),
|
|
2986
3147
|
]
|
|
2987
3148
|
|
|
2988
3149
|
|
|
@@ -11,7 +11,12 @@ from .catalog import (
|
|
|
11
11
|
provider_for_capability,
|
|
12
12
|
validate_catalog_lock,
|
|
13
13
|
)
|
|
14
|
-
from .client_config import
|
|
14
|
+
from .client_config import (
|
|
15
|
+
merge_json_mcp_servers,
|
|
16
|
+
merge_toml_mcp_servers,
|
|
17
|
+
remove_json_nexo_managed_mcp_servers,
|
|
18
|
+
remove_toml_nexo_managed_mcp_servers,
|
|
19
|
+
)
|
|
15
20
|
from .reconcile import managed_mcp_status, reconcile_managed_mcp
|
|
16
21
|
|
|
17
22
|
__all__ = [
|
|
@@ -26,6 +31,8 @@ __all__ = [
|
|
|
26
31
|
"validate_catalog_lock",
|
|
27
32
|
"merge_json_mcp_servers",
|
|
28
33
|
"merge_toml_mcp_servers",
|
|
34
|
+
"remove_json_nexo_managed_mcp_servers",
|
|
35
|
+
"remove_toml_nexo_managed_mcp_servers",
|
|
29
36
|
"managed_mcp_status",
|
|
30
37
|
"reconcile_managed_mcp",
|
|
31
38
|
]
|
|
@@ -11,6 +11,47 @@ def _is_nexo_owned(entry: Any) -> bool:
|
|
|
11
11
|
return isinstance(meta, dict) and meta.get("owner") == "nexo"
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _managed_servers_metadata(container: dict[str, Any]) -> dict[str, Any]:
|
|
15
|
+
nexo_meta = container.get("nexo") if isinstance(container.get("nexo"), dict) else {}
|
|
16
|
+
managed = nexo_meta.get("managed_mcp") if isinstance(nexo_meta.get("managed_mcp"), dict) else {}
|
|
17
|
+
servers = managed.get("servers") if isinstance(managed.get("servers"), dict) else {}
|
|
18
|
+
return servers
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _remove_nexo_owned_by_metadata(servers: dict[str, Any], managed_servers: dict[str, Any]) -> None:
|
|
22
|
+
for name, meta in list(managed_servers.items()):
|
|
23
|
+
if not (isinstance(meta, dict) and meta.get("owner") == "nexo"):
|
|
24
|
+
continue
|
|
25
|
+
current = servers.get(name)
|
|
26
|
+
if current is None or _is_nexo_owned(current) or name in servers:
|
|
27
|
+
servers.pop(name, None)
|
|
28
|
+
managed_servers.pop(name, None)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def remove_json_nexo_managed_mcp_servers(payload: dict[str, Any]) -> dict[str, Any]:
|
|
32
|
+
result = deepcopy(payload) if isinstance(payload, dict) else {}
|
|
33
|
+
servers = result.get("mcpServers")
|
|
34
|
+
if not isinstance(servers, dict):
|
|
35
|
+
return result
|
|
36
|
+
managed_servers = _managed_servers_metadata(result)
|
|
37
|
+
for name, entry in list(servers.items()):
|
|
38
|
+
if _is_nexo_owned(entry):
|
|
39
|
+
servers.pop(name, None)
|
|
40
|
+
managed_servers.pop(name, None)
|
|
41
|
+
_remove_nexo_owned_by_metadata(servers, managed_servers)
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def remove_toml_nexo_managed_mcp_servers(payload: dict[str, Any]) -> dict[str, Any]:
|
|
46
|
+
result = deepcopy(payload) if isinstance(payload, dict) else {}
|
|
47
|
+
servers = result.get("mcp_servers")
|
|
48
|
+
if not isinstance(servers, dict):
|
|
49
|
+
return result
|
|
50
|
+
managed_servers = _managed_servers_metadata(result)
|
|
51
|
+
_remove_nexo_owned_by_metadata(servers, managed_servers)
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
14
55
|
def merge_json_mcp_servers(payload: dict[str, Any], entries: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
15
56
|
result = deepcopy(payload) if isinstance(payload, dict) else {}
|
|
16
57
|
servers = result.setdefault("mcpServers", {})
|