|
@@ -26,6 +26,7 @@ from pathlib import Path
|
|
|
from typing import Any
|
|
from typing import Any
|
|
|
|
|
|
|
|
VERSION = 1
|
|
VERSION = 1
|
|
|
|
|
+PLUGIN_VERSION = "1.2.1"
|
|
|
def _plugin_fingerprint():
|
|
def _plugin_fingerprint():
|
|
|
root = Path(__file__).parent
|
|
root = Path(__file__).parent
|
|
|
digest = hashlib.sha256()
|
|
digest = hashlib.sha256()
|
|
@@ -74,6 +75,8 @@ class Binding:
|
|
|
catalog_fingerprint: str = ""
|
|
catalog_fingerprint: str = ""
|
|
|
sampling: dict = field(default_factory=dict)
|
|
sampling: dict = field(default_factory=dict)
|
|
|
computer_policy: dict | None = None
|
|
computer_policy: dict | None = None
|
|
|
|
|
+ workspace_memory: bool = False
|
|
|
|
|
+ profile_memory_tools: set = field(default_factory=set)
|
|
|
|
|
|
|
|
|
|
|
|
|
def register_context(ctx):
|
|
def register_context(ctx):
|
|
@@ -87,6 +90,8 @@ def register_context(ctx):
|
|
|
|
|
|
|
|
|
|
|
|
|
def _refresh_agent_tools(agent, binding):
|
|
def _refresh_agent_tools(agent, binding):
|
|
|
|
|
+ if binding.workspace_memory:
|
|
|
|
|
+ _isolate_profile_memory(agent, binding)
|
|
|
from model_tools import get_tool_definitions, get_toolset_for_tool
|
|
from model_tools import get_tool_definitions, get_toolset_for_tool
|
|
|
if not hasattr(agent, "_yaoyao_base_toolsets"):
|
|
if not hasattr(agent, "_yaoyao_base_toolsets"):
|
|
|
agent._yaoyao_base_toolsets = getattr(agent, "enabled_toolsets", None)
|
|
agent._yaoyao_base_toolsets = getattr(agent, "enabled_toolsets", None)
|
|
@@ -329,14 +334,14 @@ def capabilities(profile: str = "default") -> dict:
|
|
|
if not {"yaoyao_tools", "yaoyao_call"} <= names:
|
|
if not {"yaoyao_tools", "yaoyao_call"} <= names:
|
|
|
raise BridgeError(f"Profile {profile} 尚未安装或启用夭夭工具桥,请检查该 Profile 的插件与工具集设置", 503, "profile_tools_unavailable")
|
|
raise BridgeError(f"Profile {profile} 尚未安装或启用夭夭工具桥,请检查该 Profile 的插件与工具集设置", 503, "profile_tools_unavailable")
|
|
|
return {"version": VERSION, "ready": True, "in_process": True, "profile": profile, "native_tools": True,
|
|
return {"version": VERSION, "ready": True, "in_process": True, "profile": profile, "native_tools": True,
|
|
|
- "computer_runtime_version": 2, "plugin_version": "1.2.0", "plugin_fingerprint": PLUGIN_FINGERPRINT,
|
|
|
|
|
- "memory_isolation": True, "memory_extraction": True}
|
|
|
|
|
|
|
+ "computer_runtime_version": 2, "plugin_version": PLUGIN_VERSION, "plugin_fingerprint": PLUGIN_FINGERPRINT,
|
|
|
|
|
+ "memory_isolation": True, "memory_isolation_transport": "bridge-bind-v1", "memory_extraction": True}
|
|
|
finally:
|
|
finally:
|
|
|
reset_hermes_home_override(context)
|
|
reset_hermes_home_override(context)
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
reason = str(exc) if isinstance(exc, BridgeError) else "无法确认 Hermes 插件接口兼容性"
|
|
reason = str(exc) if isinstance(exc, BridgeError) else "无法确认 Hermes 插件接口兼容性"
|
|
|
return {"version": VERSION, "ready": False, "in_process": in_process, "profile": profile, "reason": reason,
|
|
return {"version": VERSION, "ready": False, "in_process": in_process, "profile": profile, "reason": reason,
|
|
|
- "plugin_version": "1.2.0", "plugin_fingerprint": PLUGIN_FINGERPRINT}
|
|
|
|
|
|
|
+ "plugin_version": PLUGIN_VERSION, "plugin_fingerprint": PLUGIN_FINGERPRINT}
|
|
|
|
|
|
|
|
|
|
|
|
|
def _string(body: dict, key: str, maximum: int = 512) -> str:
|
|
def _string(body: dict, key: str, maximum: int = 512) -> str:
|
|
@@ -431,6 +436,9 @@ def _purge(server) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
def bind(owner: tuple[str, str], body: dict, *, wait_seconds: float = 20) -> dict:
|
|
def bind(owner: tuple[str, str], body: dict, *, wait_seconds: float = 20) -> dict:
|
|
|
|
|
+ workspace_memory = body.get("workspace_memory", False)
|
|
|
|
|
+ if type(workspace_memory) is not bool:
|
|
|
|
|
+ raise BridgeError("记忆隔离设置格式无效", 400, "invalid_memory_policy")
|
|
|
policy = body.get("computer_policy")
|
|
policy = body.get("computer_policy")
|
|
|
if policy is not None and (not isinstance(policy, dict) or set(policy) != {"mode", "hostAccess"}
|
|
if policy is not None and (not isinstance(policy, dict) or set(policy) != {"mode", "hostAccess"}
|
|
|
or policy.get("mode") not in {"isolated", "profile"}
|
|
or policy.get("mode") not in {"isolated", "profile"}
|
|
@@ -484,18 +492,20 @@ def bind(owner: tuple[str, str], body: dict, *, wait_seconds: float = 20) -> dic
|
|
|
raise BridgeError("此会话尚未加载工具桥,请启用 yaoyao_bot_bridge 工具集并新建会话", 409, "tools_unavailable")
|
|
raise BridgeError("此会话尚未加载工具桥,请启用 yaoyao_bot_bridge 工具集并新建会话", 409, "tools_unavailable")
|
|
|
current = _bindings.get(sid)
|
|
current = _bindings.get(sid)
|
|
|
if current and current.generation == generation:
|
|
if current and current.generation == generation:
|
|
|
- if current.session is not session or current.agent is not agent or current.owner != owner or current.profile != profile or current.bridge_url != url or current.computer_policy != policy or not hmac.compare_digest(current.token, token):
|
|
|
|
|
|
|
+ if current.session is not session or current.agent is not agent or current.owner != owner or current.profile != profile or current.bridge_url != url or current.computer_policy != policy or current.workspace_memory != workspace_memory or not hmac.compare_digest(current.token, token):
|
|
|
raise BridgeError("不能更改现有轮次的工具授权", 409, "generation_conflict")
|
|
raise BridgeError("不能更改现有轮次的工具授权", 409, "generation_conflict")
|
|
|
current.expires_at = max(current.expires_at, expiry)
|
|
current.expires_at = max(current.expires_at, expiry)
|
|
|
current.stored_session_id = stored
|
|
current.stored_session_id = stored
|
|
|
return {"ok": True, "generation": generation, "expires_at": current.expires_at,
|
|
return {"ok": True, "generation": generation, "expires_at": current.expires_at,
|
|
|
- "native_tools": bool(current.toolset), "computer_runtime_version": 2}
|
|
|
|
|
|
|
+ "native_tools": bool(current.toolset), "computer_runtime_version": 2, "workspace_memory": current.workspace_memory}
|
|
|
previous = _generations.get(sid)
|
|
previous = _generations.get(sid)
|
|
|
seen = previous[1] if previous and previous[0] is session else set()
|
|
seen = previous[1] if previous and previous[0] is session else set()
|
|
|
if generation in seen:
|
|
if generation in seen:
|
|
|
raise BridgeError("该轮次授权已撤销", 409, "generation_revoked")
|
|
raise BridgeError("该轮次授权已撤销", 409, "generation_revoked")
|
|
|
if session.get("running"):
|
|
if session.get("running"):
|
|
|
raise BridgeError("Hermes 会话正在运行,不能更换轮次授权", 409, "session_busy")
|
|
raise BridgeError("Hermes 会话正在运行,不能更换轮次授权", 409, "session_busy")
|
|
|
|
|
+ if session.get("_yaoyao_workspace_memory") and not workspace_memory:
|
|
|
|
|
+ raise BridgeError("此 Bot 会话必须继续使用独立记忆,请新建会话后更改模式", 409, "memory_policy_conflict")
|
|
|
if len(seen) >= 4096:
|
|
if len(seen) >= 4096:
|
|
|
raise BridgeError("请恢复会话以更新工具桥授权记录", 409, "session_renewal_required")
|
|
raise BridgeError("请恢复会话以更新工具桥授权记录", 409, "session_renewal_required")
|
|
|
seen.add(generation)
|
|
seen.add(generation)
|
|
@@ -504,6 +514,10 @@ def bind(owner: tuple[str, str], body: dict, *, wait_seconds: float = 20) -> dic
|
|
|
_retire(current)
|
|
_retire(current)
|
|
|
binding = Binding(sid, stored, profile, home, generation, url, token, expiry, owner, session, agent)
|
|
binding = Binding(sid, stored, profile, home, generation, url, token, expiry, owner, session, agent)
|
|
|
binding.computer_policy = dict(policy) if policy else None
|
|
binding.computer_policy = dict(policy) if policy else None
|
|
|
|
|
+ binding.workspace_memory = workspace_memory
|
|
|
|
|
+ if workspace_memory:
|
|
|
|
|
+ _isolate_profile_memory(agent, binding)
|
|
|
|
|
+ session["_yaoyao_workspace_memory"] = True
|
|
|
if policy:
|
|
if policy:
|
|
|
# Private runtime state survives unbind so a stale tool call cannot
|
|
# Private runtime state survives unbind so a stale tool call cannot
|
|
|
# fall back to the unrestricted native Profile after revocation.
|
|
# fall back to the unrestricted native Profile after revocation.
|
|
@@ -517,7 +531,7 @@ def bind(owner: tuple[str, str], body: dict, *, wait_seconds: float = 20) -> dic
|
|
|
_retire(binding)
|
|
_retire(binding)
|
|
|
raise
|
|
raise
|
|
|
return {"ok": True, "generation": generation, "expires_at": expiry, "native_tools": bool(binding.toolset),
|
|
return {"ok": True, "generation": generation, "expires_at": expiry, "native_tools": bool(binding.toolset),
|
|
|
- "computer_runtime_version": 2}
|
|
|
|
|
|
|
+ "computer_runtime_version": 2, "workspace_memory": binding.workspace_memory}
|
|
|
|
|
|
|
|
|
|
|
|
|
def unbind(owner: tuple[str, str], body: dict) -> dict:
|
|
def unbind(owner: tuple[str, str], body: dict) -> dict:
|
|
@@ -743,6 +757,30 @@ def call_tool(args: dict, **kwargs) -> str | dict:
|
|
|
|
|
|
|
|
# Native Hermes remains the only owner of Profile configuration, skills and
|
|
# Native Hermes remains the only owner of Profile configuration, skills and
|
|
|
# authenticated integrations. The Runner never opens those files itself.
|
|
# authenticated integrations. The Runner never opens those files itself.
|
|
|
|
|
+def _isolate_profile_memory(agent, binding):
|
|
|
|
|
+ """Apply the existing agent controls before any prompt, without editing Profile config.
|
|
|
|
|
+
|
|
|
|
|
+ session.create is Hermes' strict public contract; memory isolation belongs
|
|
|
|
|
+ to this authenticated session binding instead of invented RPC parameters.
|
|
|
|
|
+ """
|
|
|
|
|
+ required = ("_memory_store", "_memory_manager", "_memory_enabled", "_user_profile_enabled", "skip_background_review")
|
|
|
|
|
+ if not all(hasattr(agent, name) for name in required) or not callable(getattr(agent, "_invalidate_system_prompt", None)):
|
|
|
|
|
+ raise BridgeError("当前 Hermes 不支持工具桥的会话记忆隔离", 409, "memory_isolation_unavailable")
|
|
|
|
|
+ manager = agent._memory_manager
|
|
|
|
|
+ binding.profile_memory_tools.add("memory")
|
|
|
|
|
+ if manager:
|
|
|
|
|
+ binding.profile_memory_tools.update(manager.get_all_tool_names())
|
|
|
|
|
+ agent._memory_store = None
|
|
|
|
|
+ agent._memory_manager = None
|
|
|
|
|
+ agent._memory_enabled = False
|
|
|
|
|
+ agent._user_profile_enabled = False
|
|
|
|
|
+ agent.skip_background_review = True
|
|
|
|
|
+ agent.disabled_toolsets = list(dict.fromkeys([*(agent.disabled_toolsets or []), "memory"]))
|
|
|
|
|
+ if manager:
|
|
|
|
|
+ manager.shutdown_all()
|
|
|
|
|
+ agent._invalidate_system_prompt()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def computer_directive(session_id=None, tool_name=None, args=None, **kwargs):
|
|
def computer_directive(session_id=None, tool_name=None, args=None, **kwargs):
|
|
|
try:
|
|
try:
|
|
|
server = _server()
|
|
server = _server()
|
|
@@ -752,7 +790,7 @@ def computer_directive(session_id=None, tool_name=None, args=None, **kwargs):
|
|
|
inherited = _children.get(session_id)
|
|
inherited = _children.get(session_id)
|
|
|
sessions = [inherited[0].session] if inherited else [s for sid, s in server._sessions.items()
|
|
sessions = [inherited[0].session] if inherited else [s for sid, s in server._sessions.items()
|
|
|
if session_id in {sid, s.get("session_key"), getattr(s.get("agent"), "session_id", None)}]
|
|
if session_id in {sid, s.get("session_key"), getattr(s.get("agent"), "session_id", None)}]
|
|
|
- managed = next((s for s in sessions if s.get("_yaoyao_computer_policy")), None)
|
|
|
|
|
|
|
+ managed = next((s for s in sessions if s.get("_yaoyao_computer_policy") or s.get("_yaoyao_workspace_memory")), None)
|
|
|
if managed is None:
|
|
if managed is None:
|
|
|
return None
|
|
return None
|
|
|
try:
|
|
try:
|
|
@@ -760,8 +798,10 @@ def computer_directive(session_id=None, tool_name=None, args=None, **kwargs):
|
|
|
except BridgeError:
|
|
except BridgeError:
|
|
|
return {"action": "block", "message": "电脑会话授权已经结束,请在当前 Bot 中发起新一轮。"}
|
|
return {"action": "block", "message": "电脑会话授权已经结束,请在当前 Bot 中发起新一轮。"}
|
|
|
policy = binding.computer_policy
|
|
policy = binding.computer_policy
|
|
|
|
|
+ if binding.workspace_memory and tool_name in binding.profile_memory_tools:
|
|
|
|
|
+ return {"action": "block", "message": "此 Bot 使用独立记忆,请使用本轮授权的 workspace 记忆工具。"}
|
|
|
if not policy:
|
|
if not policy:
|
|
|
- return {"action": "block", "message": "当前轮次缺少电脑权限声明。"}
|
|
|
|
|
|
|
+ return None if binding.workspace_memory else {"action": "block", "message": "当前轮次缺少电脑权限声明。"}
|
|
|
if policy["mode"] == "profile" or policy["hostAccess"]:
|
|
if policy["mode"] == "profile" or policy["hostAccess"]:
|
|
|
return None
|
|
return None
|
|
|
# Skill discovery, vision, and authenticated services retain the Profile's
|
|
# Skill discovery, vision, and authenticated services retain the Profile's
|