"""ai-os — CEO AI OS engine plugin for Hermes. Wires the per-user / multi-persona machinery that Hermes leaves to the product: * pre_tool_call → privacy guard: hard-block cross-user file/terminal access (see guard.py). The one thing a skill-level script cannot do. * on_session_start → resolve identity once, ensure users// exists, and upsert the registry (chat_id for nudges, last_seen). Identity & paths live in identity.py (in-process, ContextVar-aware), sharing the ``tg_`` convention + ``users//`` guardrail with core/lib/identity.py. Carries NO mentor content: personality/skill/content stay declarative. Persona loading + the /mentor command land in a later slice (P4). """ from __future__ import annotations import logging from typing import Any, Optional from . import identity as ident from . import guard as _guard from . import state as _state from . import personas as _personas from . import team as _team logger = logging.getLogger(__name__) # -- hook handlers ---------------------------------------------------------- def _on_pre_tool_call(tool_name: str = "", args: Optional[dict] = None, **kwargs: Any): """Privacy guard. Returns {"action":"block",...} to veto, else None.""" try: return _guard.check(tool_name=tool_name, args=args) except Exception as exc: # never break the tool path because of the guard logger.debug("ai-os guard error (fail-open): %s", exc) return None def _on_pre_gateway_dispatch(event: Any = None, **kwargs: Any): """Earliest per-message hook carrying full identity (event.source). Bridge it into os.environ so plugin slash-command handlers — dispatched before Hermes sets its session ContextVars — can still resolve the user. Always allows dispatch.""" try: src = getattr(event, "source", None) if src is None: return None platform = getattr(getattr(src, "platform", None), "value", "") or "" ident.stash_identity( platform=platform, user_id=getattr(src, "user_id", "") or "", chat_id=getattr(src, "chat_id", "") or "", user_name=getattr(src, "user_name", "") or "", ) uid = ident.resolve_user() if uid: ident.ensure_user_dir(uid) ident.upsert_index(uid) except Exception as exc: logger.debug("ai-os pre_gateway_dispatch skipped: %s", exc) return None def _on_session_start(session_id: str = "", **kwargs: Any) -> None: """Once per new session: ensure the chatting user's dir + registry entry.""" try: user_id = ident.resolve_user() if not user_id: return # no gateway identity (e.g. CLI) — nothing to seed ident.ensure_user_dir(user_id) ident.upsert_index(user_id) except Exception as exc: logger.debug("ai-os on_session_start skipped: %s", exc) # -- debug slash command ---------------------------------------------------- def _cmd_whoami(raw_args: str = "") -> str: """/aiwhoami — show how the engine identifies the current user (debug).""" uid = ident.resolve_user() if not uid: return ("ai-os: chưa xác định được user (không có HERMES_SESSION_USER_ID). " "Trên Telegram gateway thì luôn có; ở CLI thì không.") role = ident.org_role(uid) return "ai-os: user_id=%s · org_role=%s · dir=users/%s/" % (uid, role, uid) # -- entrypoint ------------------------------------------------------------- def register(ctx) -> None: # P1+P2 — security core ctx.register_hook("pre_tool_call", _on_pre_tool_call) ctx.register_hook("on_session_start", _on_session_start) # Identity bridge for slash commands (dispatched before session ContextVars set) ctx.register_hook("pre_gateway_dispatch", _on_pre_gateway_dispatch) ctx.register_command( "aiwhoami", handler=_cmd_whoami, description="ai-os: hiển thị định danh user hiện tại (debug).", ) # P3 — per-user state tools (engine-scoped read/write/list) _state.register(ctx) # P4 — persona registry: /experts + /mentor _personas.register(ctx) # P5 — admin-only team progress (org_role-gated) _team.register(ctx) # P6 — department task handoff from . import handoff as _handoff _handoff.register(ctx) logger.debug("ai-os registered: guard + on_session_start + state tools + persona cmds + team_progress + handoff")