-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
207 lines (165 loc) · 7.61 KB
/
Copy path__init__.py
File metadata and controls
207 lines (165 loc) · 7.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
"""Mupot Hermes backend plugin entry point.
The plugin has two intentionally separate modes:
* ``provisioner`` keeps the human-controlled Cloudflare setup tools.
* ``operator`` registers only the restricted, agent-bound Mupot action wrappers.
A single Hermes profile never receives both surfaces. Production agents use
``operator`` mode; provisioning belongs in a separate human-controlled profile.
"""
from __future__ import annotations
import json
import os
from typing import Any, Mapping
from .mupot_operator import MupotOperatorClient, OperatorSettings, register_operator_tools
from .schemas import (
MUPOT_BRAIN_ENABLE_SCHEMA,
MUPOT_PROVISION_SCHEMA,
MUPOT_STATUS_SCHEMA,
)
from .tools import mupot_brain_enable, mupot_provision, mupot_status
# Process-global registry of running inbox watchers keyed by
# (state_file, agent_id). Prevents duplicate daemon threads when the plugin is
# force-reloaded within one process; each Hermes home runs its own process.
_ACTIVE_WATCHERS: dict[tuple[str, str], Any] = {}
def _load_plugin_settings() -> dict[str, Any]:
"""Load non-secret Mupot settings from the active Hermes profile."""
try:
from hermes_cli.config import cfg_get, load_config
config = load_config()
value = cfg_get(config, "plugins", "entries", "mupot", "settings", default={})
if not isinstance(value, Mapping):
raise ValueError("plugins.entries.mupot.settings must be a mapping")
return dict(value)
except (ImportError, OSError, TypeError, ValueError) as exc:
raise RuntimeError("unable to load Mupot plugin settings; no tools were registered") from exc
def _tool_schema(name: str, description: str, parameters: dict[str, Any]) -> dict[str, Any]:
return {"name": name, "description": description, "parameters": parameters}
def _result(value: Any) -> str:
return json.dumps(value, sort_keys=True, ensure_ascii=False, default=str)
def _register_provisioner_tools(ctx: Any) -> None:
"""Register the legacy human-controlled setup surface using the current API."""
def provision(args: dict[str, Any]) -> str:
values = dict(args)
values.setdefault("cf_account_id", os.environ.get("MUPOT_CF_ACCOUNT_ID", ""))
values.setdefault("cf_api_token", os.environ.get("MUPOT_CF_API_TOKEN", ""))
return _result(mupot_provision(**values))
def status(args: dict[str, Any]) -> str:
return _result(mupot_status(**args))
def brain_enable(args: dict[str, Any]) -> str:
return _result(mupot_brain_enable(**args))
registrations = (
(
"mupot_provision",
provision,
MUPOT_PROVISION_SCHEMA,
"Idempotently plan or provision a human-owned Mupot Cloudflare deployment.",
),
(
"mupot_status",
status,
MUPOT_STATUS_SCHEMA,
"Probe a Mupot deployment health endpoint.",
),
(
"mupot_brain_enable",
brain_enable,
MUPOT_BRAIN_ENABLE_SCHEMA,
"Plan a Mupot DMN brain profile and schedule.",
),
)
for name, handler, parameters, description in registrations:
ctx.register_tool(
name=name,
handler=handler,
schema=_tool_schema(name, description, parameters),
toolset="mupot-provisioner",
)
def _register_provisioner_reminder(ctx: Any) -> None:
reminded: set[str] = set()
def on_session_start(event: Any) -> None:
session_id = str(getattr(event, "session_id", getattr(event, "id", "default")))
if session_id in reminded:
return
reminded.add(session_id)
if os.environ.get("MUPOT_CF_ACCOUNT_ID", "").strip():
return
inject = getattr(ctx, "inject_message", None)
if callable(inject):
inject(
"[mupot] Provisioner mode is active, but no Cloudflare account is configured. "
"Use mupot_provision after supplying a scoped Cloudflare credential."
)
register_hook = getattr(ctx, "register_hook", None)
if callable(register_hook):
register_hook("on_session_start", on_session_start)
legacy_on = getattr(ctx, "on", None)
if callable(legacy_on):
legacy_on("on_session_start", on_session_start)
def _maybe_start_inbox_watcher(
ctx: Any, operator_value: Mapping[str, Any], client: "MupotOperatorClient"
) -> None:
"""Start the background inbox watcher when operator settings enable it.
Fail-closed on bad watcher configuration (it is an explicit opt-in), but a
runtime delivery limitation (no inject_message surface) degrades to macOS
notifications rather than blocking registration.
"""
from .inbox_watch import InboxWatchSettings, InboxWatcher
watch_settings = InboxWatchSettings.from_mapping(operator_value)
if not watch_settings.enabled:
return
# Resolve the default state file against the ACTIVE Hermes home so two
# homes (desktop vs CLI) keep separate cursors and never race on one file.
state_file = watch_settings.state_file
if state_file == "~/.hermes/mupot-inbox-watch-state.json":
home = os.environ.get("HERMES_HOME", "").strip() or "~/.hermes"
state_file = os.path.join(home, "mupot-inbox-watch-state.json")
key = (os.path.expanduser(state_file), client.settings.agent_id)
existing = _ACTIVE_WATCHERS.get(key)
if existing is not None:
return # already running in this process (e.g. forced plugin reload)
def deliver(text: str) -> bool:
inject = getattr(ctx, "inject_message", None)
if not callable(inject):
return False
try:
return bool(inject(text))
except Exception:
return False
def mupot_inbox() -> Mapping[str, Any]:
return client.call("inbox", {"limit": 100, "peek": True})
watcher = InboxWatcher(
watch_settings,
deliver=deliver,
mupot_inbox=mupot_inbox if "mupot" in watch_settings.sources else None,
)
def on_session_start(**kwargs: Any) -> None:
# Record the live session for routing/log context. Delivery itself uses
# ctx.inject_message (CLI/desktop loop reference), not the session key.
session_id = kwargs.get("session_id")
if session_id:
watcher.set_session_key(str(session_id))
register_hook = getattr(ctx, "register_hook", None)
if callable(register_hook):
register_hook("on_session_start", on_session_start)
watcher.start()
_ACTIVE_WATCHERS[key] = watcher
def register(ctx: Any) -> None:
settings = _load_plugin_settings()
configured_mode = settings.get("mode") or os.environ.get("MUPOT_PLUGIN_MODE")
if not isinstance(configured_mode, str) or not configured_mode.strip():
raise ValueError("Mupot plugin mode must be explicitly set to 'operator' or 'provisioner'")
mode = configured_mode.strip().lower()
if mode == "operator":
operator_value = settings.get("operator", settings)
if not isinstance(operator_value, Mapping):
raise ValueError("plugins.entries.mupot.settings.operator must be a mapping")
operator_settings = OperatorSettings.from_mapping(operator_value)
token = os.environ.get("MUPOT_AGENT_TOKEN", "")
client = MupotOperatorClient(operator_settings, token=token)
register_operator_tools(ctx, client)
_maybe_start_inbox_watcher(ctx, operator_value, client)
return
if mode == "provisioner":
_register_provisioner_tools(ctx)
_register_provisioner_reminder(ctx)
return
raise ValueError("Mupot plugin mode must be 'operator' or 'provisioner'")