"""team.py — admin cross-user progress read (P5). The ONE sanctioned exception to per-user isolation: a CEO/manager may see the team's *progress* (only ``progress.md`` per persona) — never conversations, never ``artifacts/``. Gated by ``org_role`` IN CODE (not by trusting the LLM). This runs as a plugin tool handler doing its own file IO, so the pre_tool_call guard (which inspects the LLM's tool *args*) does not apply — the gate here is the explicit org_role check below. """ from __future__ import annotations import os import json from typing import Any, Dict from . import identity as ident _ADMIN_ROLES = {"ceo", "manager"} def team_progress(params: Dict[str, Any], **_: Any) -> str: uid = ident.resolve_user() if not uid: return json.dumps({"ok": False, "error": "no user identity (gateway only)"}, ensure_ascii=False) role = ident.org_role(uid) if role not in _ADMIN_ROLES: return json.dumps({"ok": False, "error": "forbidden: cần org_role ceo|manager (bạn: %s)" % role}, ensure_ascii=False) udir = ident.users_dir() idx_users = ident.load_index().get("users", {}) report = [] if os.path.isdir(udir): for entry in sorted(os.listdir(udir)): if entry.startswith("_") or entry == "_index.json": continue upath = os.path.join(udir, entry) if not os.path.isdir(upath): continue personas = {} for persona in sorted(os.listdir(upath)): ppath = os.path.join(upath, persona) if not os.path.isdir(ppath): continue prog = os.path.join(ppath, "progress.md") # ONLY progress.md — never artifacts/ or chat if os.path.isfile(prog): with open(prog, encoding="utf-8") as f: personas[persona] = f.read() if personas: report.append({ "user_id": entry, "display_name": (idx_users.get(entry, {}) or {}).get("display_name", ""), "progress": personas, }) return json.dumps({"ok": True, "count": len(report), "team": report}, ensure_ascii=False) def register(ctx) -> None: ctx.register_tool( name="team_progress_read", toolset="ai_os_admin", schema={"name": "team_progress_read", "description": "Admin (CEO/manager) only: read the team's learning progress (progress.md per user/persona). Never returns conversations or artifacts. Forbidden for staff.", "parameters": {"type": "object", "properties": {}, "required": []}}, handler=team_progress, description="Admin-only team progress overview (org_role-gated in code).")