diff --git a/.gitignore b/.gitignore index 5bb2d60b..8cccab2f 100644 --- a/.gitignore +++ b/.gitignore @@ -158,6 +158,10 @@ frontend/yarn-debug.log* frontend/yarn-error.log* frontend/pnpm-debug.log* frontend/lerna-debug.log* +# tsc build output (composite tsconfig.node.json); Vite would load vite.config.js before .ts +frontend/*.tsbuildinfo +frontend/vite.config.js +frontend/vite.config.d.ts # Keep frontend utility helpers tracked (overrides global lib/ ignore) !frontend/src/lib/ diff --git a/docsgpt/api/user/agents/routes.py b/docsgpt/api/user/agents/routes.py index 1711b039..47b1e3c7 100644 --- a/docsgpt/api/user/agents/routes.py +++ b/docsgpt/api/user/agents/routes.py @@ -25,7 +25,7 @@ from docsgpt.core.json_schema_utils import ( normalize_json_schema_payload, ) from docsgpt.core.settings import settings -from docsgpt.storage.db.base_repository import looks_like_uuid +from docsgpt.storage.db.base_repository import canonical_uuid, looks_like_uuid from docsgpt.api.user.resource_access import ( AccessDenied, agent_refs, @@ -221,17 +221,17 @@ def _denied(err: AccessDenied): return make_response(jsonify({"success": False, "message": err.message}), err.status) -def _tool_attachable(conn, tool_id: str, owner_id: str, caller: str) -> bool: - """Whether ``caller`` may attach ``tool_id`` to an agent owned by ``owner_id``. +def _tool_attachable(conn, tool_id: str, caller: str) -> bool: + """Whether ``caller`` may newly attach ``tool_id`` to an agent. - Builtin synthetic ids belong to no one. Otherwise the tool must be the - agent owner's (it runs with the owner's credentials) or reach the caller - with ``use_in_own``. + Builtin synthetic ids belong to no one. Otherwise the caller must own the + tool or reach it with ``use_in_own``. The agent owner owning it is not + enough: an editor could otherwise wire the owner's private tool (run with + the owner's credentials) into an agent the editor controls. Args: conn: Open database connection. tool_id: The tool id being attached. - owner_id: The agent's owner. caller: The user making the change. Returns: @@ -240,29 +240,26 @@ def _tool_attachable(conn, tool_id: str, owner_id: str, caller: str) -> bool: tid = str(tool_id) if is_synthesized_tool_id(tid): return True - if UserToolsRepository(conn).get_any(tid, owner_id) is not None: - return True ra = resolve(conn, "tool", tid, caller) return ra is not None and ra.can("use_in_own") -def _ref_attachable(conn, resource_type: str, resource_id: str, owner_id: str, caller: str) -> bool: - """Whether a source/prompt may be referenced by an agent owned by ``owner_id``. +def _ref_attachable(conn, resource_type: str, resource_id: str, caller: str) -> bool: + """Whether ``caller`` may newly reference a source/prompt from an agent. + + Like tools, the caller's own access counts, not the agent owner's. Args: conn: Open database connection. resource_type: ``source`` or ``prompt``. resource_id: The referenced id. - owner_id: The agent's owner. caller: The user making the change. Returns: - True when the agent owner owns it or the caller can ``use`` it. + True when the caller owns it or a team grant reaches them. """ if not resource_id: return True - if owner_id != caller and can_access(conn, resource_type, str(resource_id), owner_id): - return True return can_access(conn, resource_type, str(resource_id), caller) @@ -825,11 +822,11 @@ class CreateAgent(Resource): if src == "default": continue if looks_like_uuid(src): - extra_source_ids.append(src) + extra_source_ids.append(canonical_uuid(src)) else: source_value = data.get("source", "") if source_value and source_value != "default" and looks_like_uuid(source_value): - source_id_resolved = source_value + source_id_resolved = canonical_uuid(source_value) # Team-sharing write gate: you may reference sources/prompts you # own or that a team has shared with you directly. (Transitive @@ -854,13 +851,17 @@ class CreateAgent(Resource): # Tools run with the agent owner's credentials: attach only your # own, or ones a team lets you use in your agents. for tid in data.get("tools") or []: - if not _tool_attachable(conn, tid, user, user): + if not _tool_attachable(conn, tid, user): return make_response( jsonify({"success": False, "message": "Tool not accessible"}), 403, ) build_data = dict(data) + if isinstance(data.get("tools"), list): + build_data["tools"] = [canonical_uuid(t) for t in data["tools"]] + if looks_like_uuid(data.get("prompt_id")): + build_data["prompt_id"] = canonical_uuid(data["prompt_id"]) build_data["folder_id"] = pg_folder_id build_data["workflow_id"] = pg_workflow_id build_data["source_id"] = source_id_resolved @@ -1087,7 +1088,7 @@ class UpdateAgent(Resource): if not source_id or source_id == "default": update_fields["source_id"] = None elif looks_like_uuid(source_id): - update_fields["source_id"] = source_id + update_fields["source_id"] = canonical_uuid(source_id) else: return _reject( f"Invalid source ID format: {source_id}", user, field @@ -1102,7 +1103,7 @@ class UpdateAgent(Resource): if src == "default": continue if looks_like_uuid(src): - valid.append(src) + valid.append(canonical_uuid(src)) else: return _reject( f"Invalid source ID in list: {src}", user, field @@ -1130,7 +1131,7 @@ class UpdateAgent(Resource): tools_list = data.get("tools", []) if not isinstance(tools_list, list): return _reject("Tools must be a list", user, field) - update_fields["tools"] = tools_list + update_fields["tools"] = [canonical_uuid(t) for t in tools_list] elif field == "json_schema": json_schema = data.get("json_schema") if json_schema is not None: @@ -1250,13 +1251,25 @@ class UpdateAgent(Resource): ) if wf_err: return wf_err + # Only the owner may point the agent at a different + # workflow: editing rights on this agent extend to + # the graph it uses, so swapping in the workflow of + # another of the owner's agents would hand that + # graph to the editor. + current_workflow = existing_agent.get("workflow_id") + if is_team_editor and pg_workflow_id != ( + str(current_workflow) if current_workflow else None + ): + return _denied( + AccessDenied(403, "Only the owner can change this agent's workflow") + ) update_fields["workflow_id"] = pg_workflow_id elif field == "prompt_id": value = data["prompt_id"] if not value or value == "default": update_fields["prompt_id"] = None elif looks_like_uuid(value): - update_fields["prompt_id"] = value + update_fields["prompt_id"] = canonical_uuid(value) else: return _reject(f"Invalid prompt_id: {value}", user, field) elif field == "allow_system_prompt_override": @@ -1358,7 +1371,7 @@ class UpdateAgent(Resource): for sid in referenced_sources: if str(sid) in existing_source_refs: continue - if not _ref_attachable(conn, "source", sid, owner_id, user): + if not _ref_attachable(conn, "source", sid, user): return make_response( jsonify({"success": False, "message": "Source not accessible"}), 403 ) @@ -1366,16 +1379,17 @@ class UpdateAgent(Resource): if ( new_prompt_id and str(new_prompt_id) != str(existing_agent.get("prompt_id") or "") - and not _ref_attachable(conn, "prompt", new_prompt_id, owner_id, user) + and not _ref_attachable(conn, "prompt", new_prompt_id, user) ): return make_response( jsonify({"success": False, "message": "Prompt not accessible"}), 403 ) # Tools run with the OWNER's credentials (the agent-key path # resolves and decrypts them as the owner), so a newly attached - # tool must be the owner's or reach the caller with - # ``use_in_own``. Tools already on the agent stay. Builtin - # synthetic ids belong to no one and are always allowed. + # tool must be the caller's own or reach them with + # ``use_in_own`` -- the owner owning it is not enough. Tools + # already on the agent stay. Builtin synthetic ids belong to no + # one and are always allowed. if "tools" in update_fields: existing_tools = { str(t) for t in (existing_agent.get("tools") or []) @@ -1383,7 +1397,7 @@ class UpdateAgent(Resource): for tid in update_fields["tools"] or []: if str(tid) in existing_tools: continue - if not _tool_attachable(conn, tid, owner_id, user): + if not _tool_attachable(conn, tid, user): return make_response( jsonify( {"success": False, "message": "Tool not accessible"} diff --git a/docsgpt/api/user/resource_access.py b/docsgpt/api/user/resource_access.py index 35a19322..7d3f5627 100644 --- a/docsgpt/api/user/resource_access.py +++ b/docsgpt/api/user/resource_access.py @@ -27,7 +27,7 @@ from typing import Iterable, Optional from sqlalchemy import Connection, text -from docsgpt.storage.db.base_repository import looks_like_uuid +from docsgpt.storage.db.base_repository import canonical_uuid, looks_like_uuid from docsgpt.storage.db.repositories.agents import AgentsRepository from docsgpt.storage.db.repositories.prompts import PromptsRepository from docsgpt.storage.db.repositories.sources import SourcesRepository @@ -73,7 +73,7 @@ ACTIONS: dict[str, dict[str, str]] = { "use": "viewer", # see it and run it inside the owner's shared agents "use_in_own": "viewer", # add it to my own agents and chats "edit": "editor", # name, action descriptions, parameters, approval - "edit_credentials": "editor", # secrets, URL, auth, reconnect OAuth (write-only) + "edit_credentials": "editor", # secrets, URL, auth (write-only); OAuth servers stay owner-only "share": "owner", "delete": "owner", "manage_settings": "owner", @@ -186,14 +186,18 @@ def public_settings(resource_type: str, settings: Optional[dict]) -> list[dict]: def settings_for(conn: Connection, resource_type: str, resource_id: str) -> dict[str, bool]: """The resource's switches, defaults filled in.""" - return settings_many(conn, resource_type, [resource_id])[resource_id] + rid = canonical_uuid(str(resource_id)) + return settings_many(conn, resource_type, [rid])[rid] def settings_many( conn: Connection, resource_type: str, resource_ids: Iterable[str] ) -> dict[str, dict[str, bool]]: - """``resource_id -> switches`` for many resources in one query.""" - ids = [str(r) for r in resource_ids] + """``resource_id -> switches`` for many resources in one query. + + Keys are canonical (lowercase) UUIDs, the form Postgres returns. + """ + ids = [canonical_uuid(str(r)) for r in resource_ids] out = {rid: default_settings(resource_type) for rid in ids} uuids = [rid for rid in ids if looks_like_uuid(rid)] if not uuids: @@ -298,7 +302,10 @@ def resolve( repo_cls = _REPO_FOR_TYPE.get(resource_type) if repo_cls is None or not resource_id or not user_id: return None - owned = repo_cls(conn).get_any(str(resource_id), user_id) + # Postgres matches any casing but returns lowercase; canonicalise so the + # switch lookup (keyed by the returned id) can't miss. + resource_id = canonical_uuid(str(resource_id)) + owned = repo_cls(conn).get_any(resource_id, user_id) if owned is not None: rid = str(owned.get("id") or resource_id) return build(resource_type, rid, "owner", user_id, settings_for(conn, resource_type, rid)) diff --git a/docsgpt/api/user/tools/mcp.py b/docsgpt/api/user/tools/mcp.py index bdc9281b..c28164f1 100644 --- a/docsgpt/api/user/tools/mcp.py +++ b/docsgpt/api/user/tools/mcp.py @@ -13,6 +13,7 @@ from docsgpt.api.user.tools.routes import ( _CREDENTIALS_FOR_NEW_SERVER, _MCP_CREDENTIAL_AUTH_TYPES, _mcp_host_changed, + check_oauth_mcp_owner_only, denied_response, transform_actions, ) @@ -83,9 +84,6 @@ def _validate_mcp_server_url(config: dict) -> None: raise ValueError(f"Invalid server URL: {exc}") from exc -_ONLY_OWNER_RECONNECTS = "Only the owner can reconnect this account" - - def _existing_mcp_context(tool_id, user, config): """Resolve the stored MCP tool a test/save refers to, and its credentials. @@ -93,14 +91,16 @@ def _existing_mcp_context(tool_id, user, config): the caller needs ``edit_credentials`` on that tool and everything runs as its owner. Stored secrets are write-only, so an empty secret field reuses the stored one while the host is unchanged; a new host never inherits them. + A server that is or would become OAuth is the owner's alone (its tokens + are the owner's sign-in). Returns: ``(existing_doc, owner_id, is_owner, moved, credentials)``, or a Flask response (404 / 400) to return as is. Raises: - AccessDenied: the caller can't see the tool (404) or can't change - its credentials (403). + AccessDenied: the caller can't see the tool (404), can't change its + credentials (403), or isn't the owner of an OAuth server (403). """ auth_credentials = _extract_auth_credentials(config) if not tool_id: @@ -113,6 +113,7 @@ def _existing_mcp_context(tool_id, user, config): jsonify({"success": False, "message": "Tool not found or access denied"}), 404, ) existing_config = existing_doc.get("config") or {} + check_oauth_mcp_owner_only(ra, existing_config, config) moved = _mcp_host_changed(config, existing_config) auth_type = config.get("auth_type", "none") new_secret_keys = set(auth_credentials) - {"api_key_header"} @@ -170,13 +171,7 @@ class TestMCPServerConfig(Resource): ctx = _existing_mcp_context(data.get("id"), user, config) if not isinstance(ctx, tuple): return ctx - _existing_doc, owner_id, is_owner, _moved, auth_credentials = ctx - if not is_owner and config.get("auth_type") == "oauth": - # An OAuth flow would store tokens under the editor's account - # (and its popup event goes to that account), not the owner's. - return make_response( - jsonify({"success": False, "message": _ONLY_OWNER_RECONNECTS}), 403 - ) + _existing_doc, owner_id, _is_owner, _moved, auth_credentials = ctx test_config = config.copy() test_config["auth_credentials"] = auth_credentials @@ -279,50 +274,16 @@ class MCPServerSave(Resource): ctx = _existing_mcp_context(data.get("id"), user, config) if not isinstance(ctx, tuple): return ctx - existing_doc, owner_id, is_owner, moved, merged_credentials = ctx - existing_config = (existing_doc or {}).get("config") or {} + existing_doc, owner_id, is_owner, _moved, merged_credentials = ctx auth_type = config.get("auth_type", "none") mcp_config = config.copy() mcp_config["auth_credentials"] = merged_credentials - keep_actions = False if auth_type == "oauth": - if config.get("oauth_task_id"): - if not is_owner: - # The OAuth flow stores tokens under the account that - # ran it; reconnecting as the owner is owner-only. - return make_response( - jsonify({ - "success": False, - "message": _ONLY_OWNER_RECONNECTS, - }), - 403, - ) - redis_client = get_redis_instance() - manager = MCPOAuthManager(redis_client) - result = manager.get_oauth_status( - config["oauth_task_id"], user - ) - if not result.get("status") == "completed": - return make_response( - jsonify( - { - "success": False, - "error": "OAuth failed or not completed. Please try authorizing again.", - } - ), - 400, - ) - actions_metadata = result.get("tools", []) - elif ( - existing_doc is not None - and not moved - and existing_config.get("auth_type") == "oauth" - ): - # Editing an already-connected server: keep its tools. - actions_metadata = existing_doc.get("actions") or [] - keep_actions = True - else: + # Only the owner reaches here for an existing server (see + # ``_existing_mcp_context``), and every OAuth save needs the + # sign-in they just completed. + if not config.get("oauth_task_id"): return make_response( jsonify( { @@ -332,6 +293,22 @@ class MCPServerSave(Resource): ), 400, ) + redis_client = get_redis_instance() + manager = MCPOAuthManager(redis_client) + result = manager.get_oauth_status( + config["oauth_task_id"], user + ) + if not result.get("status") == "completed": + return make_response( + jsonify( + { + "success": False, + "error": "OAuth failed or not completed. Please try authorizing again.", + } + ), + 400, + ) + actions_metadata = result.get("tools", []) elif auth_type == "none" or merged_credentials: mcp_tool = MCPTool(config=mcp_config, user_id=owner_id) mcp_tool.discover_tools() @@ -356,8 +333,7 @@ class MCPServerSave(Resource): "redirect_uri", ]: storage_config.pop(field, None) - # Kept actions already carry the owner's on/off and approval flags. - transformed_actions = actions_metadata if keep_actions else transform_actions(actions_metadata) + transformed_actions = transform_actions(actions_metadata) display_name = data["displayName"] description = f"MCP Server: {storage_config.get('server_url', 'Unknown')}" diff --git a/docsgpt/api/user/tools/routes.py b/docsgpt/api/user/tools/routes.py index 72cbd6c3..8528ba64 100644 --- a/docsgpt/api/user/tools/routes.py +++ b/docsgpt/api/user/tools/routes.py @@ -350,6 +350,37 @@ def _mcp_host_changed(new_config: dict, existing_config: dict) -> bool: return bool(old_url) and url_host(old_url) != url_host((new_config or {}).get("server_url")) +SHARED_OAUTH_OWNER_ONLY = "Only the owner can change or reconnect this server" + + +def check_oauth_mcp_owner_only( + ra: ResourceAccess, existing_config: Optional[dict], new_config: Optional[dict] +) -> None: + """Keep a shared OAuth MCP server's connection with its owner. + + MCP OAuth tokens are looked up by owner + server URL and a shared server + runs as its owner, so a grantee who moved an OAuth server, switched a + server to OAuth, or re-ran its sign-in would be using the owner's account + somewhere the owner never chose. Until connectors own OAuth accounts, any + connection change on a server that is (or would become) OAuth is + owner-only. + + Args: + ra: The caller's access to the tool. + existing_config: The stored ``config``. + new_config: The incoming ``config``. + + Raises: + AccessDenied: 403 when a non-owner touches an OAuth server's config. + """ + if ra.access == "owner": + return + configs = [c if isinstance(c, dict) else {} for c in (existing_config, new_config)] + auth_types = {c.get("auth_type") for c in configs} + if "oauth" in auth_types: + raise AccessDenied(403, SHARED_OAUTH_OWNER_ONLY) + + def _prepare_tool_config(tool_doc: dict, new_config: dict, config_requirements: dict) -> dict: """Validate-free merge of an incoming config with the stored one, as the owner. @@ -776,6 +807,8 @@ class UpdateTool(Resource): if "config" in data: tool_name = tool_doc.get("name", data.get("name")) existing_config = tool_doc.get("config", {}) or {} + if tool_name == "mcp_tool": + check_oauth_mcp_owner_only(ra, existing_config, data["config"]) if tool_name == "api_tool" and not _api_tool_config_needs_credentials( data["config"], existing_config ): @@ -873,6 +906,7 @@ class UpdateToolConfig(Resource): tool_name = tool_doc.get("name") if tool_name == "mcp_tool": + check_oauth_mcp_owner_only(ra, tool_doc.get("config"), data["config"]) server_url = (data["config"].get("server_url") or "").strip() if server_url: try: diff --git a/docsgpt/api/user/workflows/routes.py b/docsgpt/api/user/workflows/routes.py index 1e48899a..3941e786 100644 --- a/docsgpt/api/user/workflows/routes.py +++ b/docsgpt/api/user/workflows/routes.py @@ -12,6 +12,7 @@ from docsgpt.agents.workflows.cel_evaluator import ( ) from docsgpt.api.user.resource_access import ( AccessDenied, + can_use_ref, resolve, sponsor_details, sponsors_after_save, @@ -124,6 +125,40 @@ def _node_refs(nodes: List[Dict]) -> List[Tuple[str, str]]: return refs +def _new_node_ref_denied( + conn, previous_nodes: List[Dict], new_nodes: List[Dict], caller: str +) -> Optional[AccessDenied]: + """403 for the first node tool/source ``caller`` newly adds but can't use. + + A workflow runs as its owner, so an editor saving the owner's graph must + not reference the owner's private tools or sources: the caller's own + access counts (``use_in_own`` for a tool, ``use`` for a source), not the + owner's. Refs already in the stored graph stay, like an agent's. + + Args: + conn: Open database connection. + previous_nodes: The stored graph's nodes, in builder shape. + new_nodes: The nodes being saved. + caller: The editor saving. + + Returns: + An :class:`AccessDenied` to return, or None when every new ref is fine. + """ + from docsgpt.agents.default_tools import is_synthesized_tool_id + + existing = set(_node_refs(previous_nodes)) + for resource_type, resource_id in _node_refs(new_nodes): + if (resource_type, resource_id) in existing: + continue + if resource_type == "tool" and is_synthesized_tool_id(resource_id): + continue + if resource_type == "source" and resource_id == "default": + continue + if not can_use_ref(conn, resource_type, resource_id, caller): + return AccessDenied(403, f"{resource_type.capitalize()} not accessible") + return None + + def _denied(err: AccessDenied): """403/404 in this module's ``error`` shape, plus the shared ``message`` key.""" return make_response( @@ -639,6 +674,16 @@ class WorkflowDetail(Resource): nodes_data = normalize_agent_node_json_schemas(nodes_data) pg_workflow_id = str(workflow["id"]) current_graph_version = get_workflow_graph_version(workflow) + if acting != user_id: + previous_nodes = [ + serialize_node(n) + for n in WorkflowNodesRepository(conn).find_by_version( + pg_workflow_id, current_graph_version, + ) + ] + denied = _new_node_ref_denied(conn, previous_nodes, nodes_data, user_id) + if denied is not None: + return _denied(denied) next_graph_version = current_graph_version + 1 _write_graph( diff --git a/docsgpt/storage/db/base_repository.py b/docsgpt/storage/db/base_repository.py index 3a8bee0d..9d92dbe3 100644 --- a/docsgpt/storage/db/base_repository.py +++ b/docsgpt/storage/db/base_repository.py @@ -35,6 +35,24 @@ def looks_like_uuid(value: Any) -> bool: return isinstance(value, str) and bool(_UUID_RE.match(value)) +def canonical_uuid(value: Any) -> Any: + """The lowercase canonical form of a UUID string; anything else unchanged. + + Postgres accepts any casing on ``CAST(... AS uuid)`` but returns the + lowercase form, so an id used as a dict key or stored in a JSON/array + column must be canonical to match what the database hands back. + + Args: + value: A candidate id. + + Returns: + ``str(UUID(value))`` for a UUID, else ``value`` as given. + """ + if isinstance(value, UUID): + return str(value) + return str(UUID(value)) if looks_like_uuid(value) else value + + def row_to_dict(row: Any) -> dict: """Convert a SQLAlchemy ``Row`` to a plain JSON-safe dict. diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index 4232dde4..63bfedc4 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -1054,7 +1054,7 @@ "password": "Passwort" }, "savedKeyHint": "Ein Schlüssel ist gespeichert. Leer lassen, um ihn zu behalten (nur solange der Server unverändert ist).", - "sharedOAuthOwnerOnly": "Nur der Eigentümer kann die Anmeldung neu verbinden. Du kannst das Tool umbenennen, aber weder Server noch Konto ändern." + "sharedOAuthOwnerOnly": "Nur der Eigentümer kann die Verbindung dieses Servers ändern oder die Anmeldung neu verbinden. Du kannst ihn weiterhin umbenennen und seine Aktionen bearbeiten." }, "configErrors": { "required": "{{field}} ist erforderlich", diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 1d651430..3968ff98 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -1060,7 +1060,7 @@ "password": "password" }, "savedKeyHint": "A key is saved. Leave empty to keep it (only while the server is unchanged).", - "sharedOAuthOwnerOnly": "Only the owner can reconnect its sign-in, so you can rename it but not change its server or account." + "sharedOAuthOwnerOnly": "Only the owner can change this server's connection or reconnect its sign-in. You can still rename it and edit its actions." }, "configErrors": { "required": "{{field}} is required", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index fec0c542..3b06f31e 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -1054,7 +1054,7 @@ "password": "contraseña" }, "savedKeyHint": "Hay una clave guardada. Déjalo vacío para conservarla (solo mientras el servidor no cambie).", - "sharedOAuthOwnerOnly": "Solo el propietario puede volver a conectar su inicio de sesión, así que puedes cambiarle el nombre, pero no su servidor ni su cuenta." + "sharedOAuthOwnerOnly": "Solo el propietario puede cambiar la conexión de este servidor o volver a conectar su inicio de sesión. Aún puedes cambiarle el nombre y editar sus acciones." }, "configErrors": { "required": "{{field}} es obligatorio", diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index e065eb0d..94dd1bbf 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -1053,7 +1053,7 @@ "password": "パスワード" }, "savedKeyHint": "キーが保存されています。空のままにすると保持されます(サーバーが変わらない場合のみ)。", - "sharedOAuthOwnerOnly": "サインインを再接続できるのはオーナーだけです。名前は変更できますが、サーバーやアカウントは変更できません。" + "sharedOAuthOwnerOnly": "このサーバーの接続を変更したり、サインインを再接続したりできるのはオーナーだけです。名前の変更とアクションの編集は引き続き行えます。" }, "configErrors": { "required": "{{field}}は必須です", diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 85b1af45..8d1a61e2 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -1104,7 +1104,7 @@ "password": "пароль" }, "savedKeyHint": "Ключ сохранён. Оставьте поле пустым, чтобы сохранить его (только пока сервер не изменился).", - "sharedOAuthOwnerOnly": "Переподключить вход может только владелец: вы можете переименовать инструмент, но не менять его сервер или аккаунт." + "sharedOAuthOwnerOnly": "Изменить подключение этого сервера или переподключить вход может только владелец. Вы по-прежнему можете переименовать его и изменять его действия." }, "configErrors": { "required": "Поле «{{field}}» обязательно", diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index fc735107..ddbe792e 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -1053,7 +1053,7 @@ "password": "密碼" }, "savedKeyHint": "已儲存金鑰。留空即可保留(僅在伺服器未變更時)。", - "sharedOAuthOwnerOnly": "只有擁有者可以重新連結其登入,因此你可以重新命名,但不能變更其伺服器或帳戶。" + "sharedOAuthOwnerOnly": "只有擁有者可以變更此伺服器的連線或重新連結其登入。你仍然可以重新命名並編輯其動作。" }, "configErrors": { "required": "{{field}}為必填項", diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 393cb5d2..2bdc5001 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -1053,7 +1053,7 @@ "password": "密码" }, "savedKeyHint": "已保存密钥。留空即可保留(仅在服务器未更改时)。", - "sharedOAuthOwnerOnly": "只有所有者可以重新连接其登录,因此你可以重命名它,但不能更改其服务器或账户。" + "sharedOAuthOwnerOnly": "只有所有者可以更改此服务器的连接或重新连接其登录。你仍然可以重命名它并编辑其操作。" }, "configErrors": { "required": "{{field}}为必填项", diff --git a/frontend/src/modals/MCPServerModal.test.tsx b/frontend/src/modals/MCPServerModal.test.tsx index d3a98311..b7a1c8c8 100644 --- a/frontend/src/modals/MCPServerModal.test.tsx +++ b/frontend/src/modals/MCPServerModal.test.tsx @@ -184,8 +184,7 @@ describe('MCPServerModal', () => { expect(text()).toContain('Invalid server URL'); }); - it('lets an editor rename an OAuth tool without reconnecting it', async () => { - saveMCPServer.mockReturnValue(json({ success: true })); + it('keeps a shared OAuth server read-only for an editor', async () => { await render({ access: 'editor', owner_label: 'Lena', @@ -196,10 +195,9 @@ describe('MCPServerModal', () => { expect(urlInput().disabled).toBe(true); expect(button('settings.tools.mcp.testConnection')).toBeUndefined(); const save = button('settings.tools.mcp.save'); - expect(save.disabled).toBe(false); + expect(save.disabled).toBe(true); await act(async () => save.click()); - expect(saveMCPServer).toHaveBeenCalledTimes(1); - expect(testMCPConnection).not.toHaveBeenCalled(); + expect(saveMCPServer).not.toHaveBeenCalled(); }); it('keeps OAuth reconnect for the owner', async () => { diff --git a/frontend/src/modals/MCPServerModal.tsx b/frontend/src/modals/MCPServerModal.tsx index 975ec1c0..accc16c7 100644 --- a/frontend/src/modals/MCPServerModal.tsx +++ b/frontend/src/modals/MCPServerModal.tsx @@ -114,8 +114,9 @@ export default function MCPServerModal({ // A tool shared with the caller (an editor reconnecting the owner's // server): its saved secrets stay hidden and a new entry replaces them. const isShared = !!server?.access && server.access !== 'owner'; - // Only the owner can re-run an OAuth sign-in (the tokens are theirs), so a - // teammate may rename an OAuth tool but not change its server or account. + // Only the owner can change or re-run an OAuth server's sign-in (the tokens + // are theirs), so the modal is read-only for a teammate. The Tools menu + // doesn't offer it to them; this guards any other way in. const oauthOwnerOnly = isShared && server?.auth_type === 'oauth'; const savedSecret = SECRET_FIELDS[formData.auth_type]; const hasSavedSecret = @@ -652,7 +653,7 @@ export default function MCPServerModal({ submitLabel={t('settings.tools.mcp.save')} onSubmit={handleSave} pending={loading} - disabled={!saveActive && !oauthOwnerOnly} + disabled={!saveActive || oauthOwnerOnly} /> } > diff --git a/frontend/src/settings/ToolConfig.test.tsx b/frontend/src/settings/ToolConfig.test.tsx index c91f233c..e9fb5756 100644 --- a/frontend/src/settings/ToolConfig.test.tsx +++ b/frontend/src/settings/ToolConfig.test.tsx @@ -183,6 +183,63 @@ describe('ToolConfig', () => { expect(alert?.className).toContain('text-destructive'); }); + describe('a shared MCP server', () => { + const mcpTool = (authType: string) => + ({ + id: 'mcp-1', + name: 'mcp_tool', + displayName: 'MCP', + description: '', + status: true, + access: 'editor', + allowed_actions: ['edit', 'edit_credentials', 'use', 'use_in_own'], + config: { + server_url: 'https://mcp.example.com/mcp', + auth_type: authType, + }, + configRequirements: { + server_url: { type: 'string', label: 'Server URL', secret: false }, + auth_type: { type: 'string', label: 'Auth', secret: false }, + }, + actions: [], + }) as unknown as UserToolType; + + const rename = async () => { + const name = container.querySelector( + 'input[placeholder="settings.tools.customNamePlaceholder"]', + ); + await act(async () => { + const setter = Object.getOwnPropertyDescriptor( + HTMLInputElement.prototype, + 'value', + )?.set; + setter?.call(name, 'Renamed'); + name?.dispatchEvent(new Event('input', { bubbles: true })); + }); + await act(async () => { + buttonByText('settings.tools.save')?.click(); + }); + }; + + it('locks the connection of an OAuth server and saves without it', async () => { + updateTool.mockResolvedValue({ ok: true }); + await render(mcpTool('oauth')); + expect(container.querySelector('fieldset')?.disabled).toBe(true); + await rename(); + expect(updateTool).toHaveBeenCalledTimes(1); + expect(updateTool.mock.calls[0][0]).not.toHaveProperty('config'); + expect(updateTool.mock.calls[0][0].customName).toBe('Renamed'); + }); + + it('still lets an editor change a non-OAuth server', async () => { + updateTool.mockResolvedValue({ ok: true }); + await render(mcpTool('bearer')); + expect(container.querySelector('fieldset')?.disabled).toBe(false); + await rename(); + expect(updateTool.mock.calls[0][0]).toHaveProperty('config'); + }); + }); + it('renders the API tool header actions as outline-primary pills', async () => { await render(apiTool); for (const label of [ diff --git a/frontend/src/settings/ToolConfig.tsx b/frontend/src/settings/ToolConfig.tsx index fc7598d2..d9eeba9a 100644 --- a/frontend/src/settings/ToolConfig.tsx +++ b/frontend/src/settings/ToolConfig.tsx @@ -41,6 +41,7 @@ import { ActiveState } from '../models/misc'; import { selectToken } from '../preferences/preferenceSlice'; import { getMethodBadgeVariant } from '../utils/httpMethodColors'; import { can } from '../utils/accessUtils'; +import { isSharedOAuthMcp } from '../utils/toolUtils'; import { areObjectsEqual } from '../utils/objectUtils'; import { cn, focusRing } from '@/lib/utils'; import { APIActionType, APIToolType, UserToolType } from './types'; @@ -121,7 +122,10 @@ export default function ToolConfig({ >(new Set()); const { t } = useTranslation(); const canEdit = can(tool, 'edit'); - const canEditCredentials = can(tool, 'edit_credentials'); + // A shared OAuth server's connection stays with its owner (the backend + // refuses it), so its fields lock like credentials the caller can't change. + const sharedOAuth = isSharedOAuthMcp(tool); + const canEditCredentials = can(tool, 'edit_credentials') && !sharedOAuth; // Neither: the tool opens as a read-only view with no Save. const readOnly = !canEdit && !canEditCredentials; const access = React.useMemo( @@ -274,7 +278,10 @@ export default function ToolConfig({ displayName: tool.displayName, customName: customName, description: tool.description, - config: configToSave, + // Locked config isn't sent, so a rename or action edit still saves. + ...((canEditCredentials || tool.name === 'api_tool') && { + config: configToSave, + }), actions: 'actions' in tool ? tool.actions : [], status: tool.status, }, @@ -398,7 +405,13 @@ export default function ToolConfig({ {readOnly && } {!readOnly && !canEditCredentials && ( - + )} {saveError && ( diff --git a/frontend/src/settings/Tools.test.tsx b/frontend/src/settings/Tools.test.tsx index 141d8b42..8421017e 100644 --- a/frontend/src/settings/Tools.test.tsx +++ b/frontend/src/settings/Tools.test.tsx @@ -183,6 +183,16 @@ describe('Tools', () => { ]); }); + it("hides Reconnect from an editor of an OAuth server (the sign-in is the owner's)", async () => { + const oauthConfig = { ...baseTool.config, auth_type: 'oauth' }; + await render([ + { ...editorTool, config: oauthConfig }, + { ...ownTool, config: oauthConfig }, + ]); + expect(menuLabels('ed')).toEqual(['settings.tools.edit']); + expect(menuLabels('own')).toContain('settings.tools.reconnect'); + }); + it('shows only View to a viewer, which opens the config read-only', async () => { await render([viewerTool]); expect(menuLabels('vw')).toEqual(['settings.tools.view']); diff --git a/frontend/src/settings/Tools.tsx b/frontend/src/settings/Tools.tsx index 6e8a71b6..05dad368 100644 --- a/frontend/src/settings/Tools.tsx +++ b/frontend/src/settings/Tools.tsx @@ -26,7 +26,11 @@ import { showActionToast } from '../notifications/actionToastSlice'; import { selectToken } from '../preferences/preferenceSlice'; import ShareToTeamModal from '../teams/ShareToTeamModal'; import { can, isOwner, roleOf } from '../utils/accessUtils'; -import { canAddToolToOwn, toolInChat } from '../utils/toolUtils'; +import { + canAddToolToOwn, + isSharedOAuthMcp, + toolInChat, +} from '../utils/toolUtils'; import RemoteDeviceConfig from './RemoteDeviceConfig'; import ToolConfig from './ToolConfig'; import { APIToolType, UserToolType } from './types'; @@ -140,7 +144,12 @@ export default function Tools() { variant: 'default', }, ]; - if (tool.name === 'mcp_tool' && can(tool, 'edit_credentials')) { + // A shared OAuth server's sign-in is the owner's to redo. + if ( + tool.name === 'mcp_tool' && + can(tool, 'edit_credentials') && + !isSharedOAuthMcp(tool) + ) { options.push({ icon: RefreshCw, label: t('settings.tools.reconnect'), diff --git a/frontend/src/utils/toolUtils.test.ts b/frontend/src/utils/toolUtils.test.ts index a389cd72..76fe093c 100644 --- a/frontend/src/utils/toolUtils.test.ts +++ b/frontend/src/utils/toolUtils.test.ts @@ -6,6 +6,7 @@ import { isChatPickerToolVisible, isChatToolVisible, isClassicAgentToolVisible, + isSharedOAuthMcp, toolInChat, } from './toolUtils'; @@ -88,6 +89,26 @@ describe('canAddToolToOwn', () => { }); }); +describe('isSharedOAuthMcp', () => { + const oauth = { name: 'mcp_tool', config: { auth_type: 'oauth' } }; + + it('is true only for an OAuth MCP server shared with the caller', () => { + expect(isSharedOAuthMcp({ ...oauth, access: 'editor' })).toBe(true); + expect(isSharedOAuthMcp({ ...oauth, access: 'owner' })).toBe(false); + expect(isSharedOAuthMcp(oauth)).toBe(false); + expect( + isSharedOAuthMcp({ + name: 'mcp_tool', + access: 'editor', + config: { auth_type: 'bearer' }, + }), + ).toBe(false); + expect( + isSharedOAuthMcp({ ...oauth, name: 'api_tool', access: 'editor' }), + ).toBe(false); + }); +}); + describe('isChatPickerToolVisible', () => { it('hides shared tools the caller may not add to their own chats', () => { expect( diff --git a/frontend/src/utils/toolUtils.ts b/frontend/src/utils/toolUtils.ts index 3515fd07..36370281 100644 --- a/frontend/src/utils/toolUtils.ts +++ b/frontend/src/utils/toolUtils.ts @@ -51,6 +51,18 @@ export const toolInChat = (tool: { export const canAddToolToOwn = (tool: AccessFields): boolean => isOwner(tool) || can(tool, 'use_in_own'); +/** + * A team-shared MCP server that signs in with OAuth. Its connection (URL, + * auth, sign-in) is the owner's alone, since the tokens are the owner's + * account; the backend refuses any change from anyone else. + */ +export const isSharedOAuthMcp = ( + tool: AccessFields & { name?: string; config?: unknown }, +): boolean => + tool.name === 'mcp_tool' && + !isOwner(tool) && + (tool.config as { auth_type?: string } | undefined)?.auth_type === 'oauth'; + // Composer Tools picker: the chat-popup rule, minus shared tools the caller // can't turn on for their own chats. export const isChatPickerToolVisible = ( diff --git a/frontend/tsconfig.node.tsbuildinfo b/frontend/tsconfig.node.tsbuildinfo deleted file mode 100644 index 4bd24cc2..00000000 --- a/frontend/tsconfig.node.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.es2025.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.dom.asynciterable.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.es2025.collection.d.ts","./node_modules/typescript/lib/lib.es2025.float16.d.ts","./node_modules/typescript/lib/lib.es2025.intl.d.ts","./node_modules/typescript/lib/lib.es2025.iterator.d.ts","./node_modules/typescript/lib/lib.es2025.promise.d.ts","./node_modules/typescript/lib/lib.es2025.regexp.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/typescript/lib/lib.es2025.full.d.ts","./node_modules/vitest/optional-runtime-types.d.ts","./node_modules/tinybench/dist/index.d.ts","./node_modules/vitest/dist/chunks/config.d.cu_b-wjj.d.ts","./node_modules/vite/types/hmrpayload.d.ts","./node_modules/vite/dist/node/chunks/modulerunnertransport.d.ts","./node_modules/vite/types/customevent.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/module-runner.d.ts","./node_modules/vitest/dist/chunks/rpc.d.da9utv4e.d.ts","./node_modules/vitest/dist/chunks/environment.d.c6xyahwa.d.ts","./node_modules/vitest/dist/chunks/worker.d.mlmnzoje.d.ts","./node_modules/vitest/dist/chunks/browser.d.g5thl309.d.ts","./node_modules/vitest/dist/chunks/task-utils.d.bzm4gsqd.d.ts","./node_modules/@vitest/mocker/dist/registry.d-xlx_foyf.d.ts","./node_modules/@vitest/mocker/dist/index.d-d4wtotqw.d.ts","./node_modules/@vitest/mocker/dist/index.d.ts","./node_modules/vitest/dist/chunks/evaluatedmodules.d.bxj5omdx.d.ts","./node_modules/expect-type/dist/utils.d.ts","./node_modules/expect-type/dist/overloads.d.ts","./node_modules/expect-type/dist/branding.d.ts","./node_modules/expect-type/dist/messages.d.ts","./node_modules/expect-type/dist/index.d.ts","./node_modules/@types/deep-eql/index.d.ts","./node_modules/assertion-error/index.d.ts","./node_modules/@types/chai/index.d.ts","./node_modules/vitest/dist/index.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/web-globals/streams.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/rolldown/dist/shared/logging-xuho4may.d.mts","./node_modules/@oxc-project/types/types.d.ts","./node_modules/rolldown/dist/shared/binding-dzunhvw4.d.mts","./node_modules/@rolldown/pluginutils/dist/filter/index.d.mts","./node_modules/@rolldown/pluginutils/dist/index.d.mts","./node_modules/rolldown/dist/shared/define-config-djhybh6s.d.mts","./node_modules/rolldown/dist/index.d.mts","./node_modules/rolldown/dist/parse-ast-index.d.mts","./node_modules/vite/types/internal/rolluptypecompat.d.ts","./node_modules/rolldown/dist/shared/constructors-bnig8fec.d.mts","./node_modules/rolldown/dist/plugins-index.d.mts","./node_modules/rolldown/dist/shared/transform-cvse9_mh.d.mts","./node_modules/rolldown/dist/utils-index.d.mts","./node_modules/vite/types/internal/devtoolsoptions.d.ts","./node_modules/vite/types/internal/esbuildoptions.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/rolldown/dist/experimental-index.d.mts","./node_modules/vite/types/internal/terseroptions.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/vite/types/internal/csspreprocessoroptions.d.ts","./node_modules/vite/node_modules/lightningcss/node/ast.d.ts","./node_modules/vite/node_modules/lightningcss/node/targets.d.ts","./node_modules/vite/node_modules/lightningcss/node/index.d.ts","./node_modules/vite/types/internal/lightningcssoptions.d.ts","./node_modules/rolldown/dist/filter-index.d.mts","./node_modules/vite/types/importglob.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/@vitejs/plugin-react/types/optionaltypes.d.ts","./node_modules/@vitejs/plugin-react/dist/index.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/@rollup/pluginutils/types/index.d.ts","./node_modules/prettier/doc.d.ts","./node_modules/prettier/index.d.ts","./node_modules/@svgr/babel-plugin-transform-svg-component/dist/index.d.ts","./node_modules/@svgr/babel-preset/dist/index.d.ts","./node_modules/@svgr/core/dist/index.d.ts","./node_modules/vite-plugin-svgr/dist/index.d.ts","./vite.config.ts"],"fileIdsList":[[114,163,180,181],[114,163,180,181,217],[114,163,180,181,262],[114,163,180,181,266],[114,163,180,181,265,267],[105,106,114,163,180,181],[114,160,161,163,180,181],[114,162,163,180,181],[163,180,181],[114,163,168,180,181,198],[114,163,164,169,174,180,181,183,195,206],[114,163,164,165,174,180,181,183],[109,110,111,114,163,180,181],[114,163,166,180,181,207],[114,163,167,168,175,180,181,184],[114,163,168,180,181,195,203],[114,163,169,171,174,180,181,183],[114,162,163,170,180,181],[114,163,171,172,180,181],[114,163,173,174,180,181],[114,162,163,174,180,181],[114,163,174,175,176,180,181,195,206],[114,163,174,175,176,180,181,190,195,198],[114,155,163,171,174,177,180,181,183,195,206],[114,163,174,175,177,178,180,181,183,195,203,206],[114,163,177,179,180,181,195,203,206],[112,113,114,115,116,117,118,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,208,209,210,211,212],[114,163,174,180,181],[114,163,180,181,182,206],[114,163,171,174,180,181,183,195],[114,163,180,181,184],[114,163,180,181,185],[114,162,163,180,181,186],[114,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,208,209,210,211,212],[114,163,180,181,188],[114,163,180,181,189],[114,163,174,180,181,190,191],[114,163,180,181,190,192,207,209],[114,163,175,180,181],[114,163,174,180,181,195,196,198],[114,163,180,181,197,198],[114,163,180,181,195,196],[114,163,180,181,198],[114,163,180,181,199],[114,160,163,180,181,195,200,206],[114,163,174,180,181,201,202],[114,163,180,181,201,202],[114,163,168,180,181,183,195,203],[114,163,180,181,204],[114,163,180,181,183,205],[114,163,177,180,181,189,206],[114,163,168,180,181,207],[114,163,180,181,195,208],[114,163,180,181,182,209],[114,163,180,181,210],[114,155,163,180,181],[114,155,163,174,176,180,181,186,195,198,206,208,209,211],[114,163,180,181,195,212],[114,163,180,181,259,260],[96,114,163,180,181],[96,97,114,163,180,181],[100,101,114,163,180,181],[100,101,102,103,114,163,180,181],[100,102,114,163,180,181],[100,114,163,180,181],[114,163,180,181,247],[114,163,180,181,245,247],[114,163,180,181,236,244,245,246,248,250],[114,163,180,181,234],[114,163,180,181,237,242,247,250],[114,163,180,181,233,250],[114,163,180,181,237,238,241,242,243,250],[114,163,180,181,237,238,239,241,242,250],[114,163,180,181,234,235,236,237,238,242,243,244,246,247,248,250],[114,163,180,181,250],[114,163,180,181,232,234,235,236,237,238,239,241,242,243,244,245,246,247,248,249],[114,163,180,181,232,250],[114,163,180,181,237,239,240,242,243,250],[114,163,180,181,241,250],[114,163,180,181,242,243,247,250],[114,163,180,181,235,245],[114,163,180,181,264],[114,163,180,181,216,219,223,225],[114,163,180,181,219],[114,163,180,181,214,216,219],[114,163,180,181,215,216],[114,163,180,181,216,219,223],[114,163,180,181,215],[114,163,180,181,216,219],[114,163,180,181,214,215,216,218],[114,163,180,181,214,216],[114,163,180,181,215,216,225],[114,127,131,163,180,181,206],[114,127,163,180,181,195,206],[114,122,163,180,181],[114,124,127,163,180,181,203,206],[114,163,180,181,183,203],[114,163,180,181,213],[114,122,163,180,181,213],[114,124,127,163,180,181,183,206],[114,119,120,123,126,163,174,180,181,195,206],[114,127,134,163,180,181],[114,119,125,163,180,181],[114,127,148,149,163,180,181],[114,123,127,163,180,181,198,206,213],[114,148,163,180,181,213],[114,121,122,163,180,181,213],[114,127,163,180,181],[114,121,122,123,124,125,126,127,128,129,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,149,150,151,152,153,154,163,180,181],[114,127,142,163,180,181],[114,127,134,135,163,180,181],[114,125,127,135,136,163,180,181],[114,126,163,180,181],[114,119,122,127,163,180,181],[114,127,131,135,136,163,180,181],[114,131,163,180,181],[114,125,127,130,163,180,181,206],[114,119,124,127,134,163,180,181],[114,163,180,181,195],[114,122,127,148,163,180,181,211,213],[114,163,180,181,259,263,268],[86,114,163,180,181],[86,87,88,90,114,163,174,175,177,178,179,180,181,183,195,203,206,212,213,220,221,222,224,226,227,228,229,230,231,251,252,256,257,258,259],[86,87,88,89,114,163,180,181],[114,163,180,181,253,254],[88,114,163,180,181],[114,163,180,181,255],[114,163,180,181,220,229,259],[114,163,180,181,220,259],[85,91,93,114,163,180,181],[83,84,114,163,180,181],[85,114,163,180,181],[90,114,163,180,181],[85,90,114,163,180,181],[85,108,114,163,180,181],[85,90,91,92,114,163,180,181],[83,84,85,90,91,92,93,94,95,98,99,104,107,114,163,180,181],[108,114,163,180,181,185,259,261,269]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"1e9332c23e9a907175e0ffc6a49e236f97b48838cc8aec9ce7e4cec21e544b65","impliedFormat":1},{"version":"3753fbc1113dc511214802a2342280a8b284ab9094f6420e7aa171e868679f91","impliedFormat":1},{"version":"999ca32883495a866aa5737fe1babc764a469e4cde6ee6b136a4b9ae68853e4b","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"2a2de5b9459b3fc44decd9ce6100b72f1b002ef523126c1d3d8b2a4a63d74d78","affectsGlobalScope":true,"impliedFormat":1},{"version":"f13f4b465c99041e912db5c44129a94588e1aafee35a50eab51044833f50b4ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"0bd714129fca875f7d4c477a1a392200b0bcd13fb2e80928cd334b63830ea047","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2c9037ae6cd2c52d80ceef0b3c5ffdb488627d71529cf4f63776daf11161c9a","affectsGlobalScope":true,"impliedFormat":1},{"version":"135d5cf4d345f59f1a9caadfafcd858d3d9cc68290db616cc85797224448cccc","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc238c3f81c2984751932b6aab223cd5b830e0ac6cad76389e5e9d2ffc03287d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4a07f9b76d361f572620927e5735b77d6d2101c23cdd94383eb5b706e7b36357","affectsGlobalScope":true,"impliedFormat":1},{"version":"7c4e8dc6ab834cc6baa0227e030606d29e3e8449a9f67cdf5605ea5493c4db29","affectsGlobalScope":true,"impliedFormat":1},{"version":"de7ba0fd02e06cd9a5bd4ab441ed0e122735786e67dde1e849cced1cd8b46b78","affectsGlobalScope":true,"impliedFormat":1},{"version":"6148e4e88d720a06855071c3db02069434142a8332cf9c182cda551adedf3156","affectsGlobalScope":true,"impliedFormat":1},{"version":"d63dba625b108316a40c95a4425f8d4294e0deeccfd6c7e59d819efa19e23409","affectsGlobalScope":true,"impliedFormat":1},{"version":"0568d6befee03dd435bed4fc25c4e46865b24bdcb8c563fdc21f580a2c301904","affectsGlobalScope":true,"impliedFormat":1},{"version":"30d62269b05b584741f19a5369852d5d34895aa2ac4fd948956f886d15f9cc0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"ffbe6d7b295306b2ba88030f65b74c107d8d99bdcf596ea99c62a02f606108b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"996fb27b15277369c68a4ba46ed138b4e9e839a02fb4ec756f7997629242fd9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"79b712591b270d4778c89706ca2cfc56ddb8c3f895840e477388f1710dc5eda9","affectsGlobalScope":true,"impliedFormat":1},{"version":"20884846cef428b992b9bd032e70a4ef88e349263f63aeddf04dda837a7dba26","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"e03da518b01b46a4c99a1f88cd727ee98ddf14492c43dae1ae7a63e992971bab","impliedFormat":1},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"883ab61941ae697bd5724f02abc0a4044dc5427e7fb002c95867204918c1a68c","impliedFormat":99},{"version":"2493d114a1f01f02d69b4b44265405dfd5bbf135e0f467838808c4137c71ce54","impliedFormat":99},{"version":"24c010698f5f7a0f1c216b963994d09ecfef77d7d5b4116b248068287ddb5964","impliedFormat":99},{"version":"c911b3bd083dade8fdb7775261aa25feae695d562a3b11209650b47be90043fc","impliedFormat":99},{"version":"5ef1bf71c773ca3df07f0c8c126af004a8e7310a7373ba4f049e7d3fb1b21921","impliedFormat":99},{"version":"4e003c868b0d8f8ad200b96cbc653e18e513fa23e1c19c4fe3cc25d4394efc47","impliedFormat":99},{"version":"93aed5bd196d1a1417d07e5c5b66a96107f0ee7d5c9027c2af12d876ab5e0f5e","impliedFormat":99},{"version":"fc620fc5a05d1d8499e790626f83b401604634dfd9c4b11042099a63883df8fb","impliedFormat":99},{"version":"123b46e873f6ae5902094478c2c58b725b3d92e1d36b53daf85b7732d032dfa8","impliedFormat":99},{"version":"11f54bcce4d6e4271122c6b06329b37f6d297141ad6e918b5181e17572158a10","impliedFormat":99},{"version":"dd8cb59c27a49fdc4b86eb755ddaaab5de0f30a627767f506fb2607ca4b2ec22","impliedFormat":99},{"version":"9500d6fd10c59daeab4a18b1d9aeaf13d790bd4a92a5f864d5f323dba395d4cc","impliedFormat":99},{"version":"8d4ea2431523a42682a9f2efb2ae662aa42a39983c9d40a19257b2445f50450e","impliedFormat":99},{"version":"29738162d88967e51557276cba80552cb8d2a4c823873b7f1531244860f0d178","impliedFormat":99},{"version":"fc07122b30042636bbf689305c75a764d6e3c2eeb75cd1033c96a6a7dc9e6e3f","impliedFormat":99},{"version":"b413fbc6658fe2774f8bf9a15cf4c53e586fc38a2d5256b3b9647da242c14389","impliedFormat":99},{"version":"dd51e53752b310bd20c9b1a87bbf12b1fe2be7fe40f505b43199496481096275","impliedFormat":1},{"version":"a87be4662442b3feeffc331ecafe6b36cafd08727e2d7f2425a5099577e7fd18","impliedFormat":1},{"version":"cd4cd9220a1ba793bc935e76d8e5481c110a90d9868ae7866a182ee71cdb6abb","impliedFormat":1},{"version":"0a7fb8619b10bc05fd933ca9ac1c8b2ab2220be7a57b57565c3ac158595494ef","impliedFormat":1},{"version":"c4a5f91feb9c5a6b2a91089d959c38391b79a961db3b9cc73b8877d57ad7dcdc","impliedFormat":1},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"3589cb90a45acfb8c2336b34a9e340f036a6aecfdb71742047d37748dfa52816","impliedFormat":99},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d97fb21da858fb18b8ae72c314e9743fd52f73ebe2764e12af1db32fc03f853f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"78dbea00e90d2df8ea3dbef0cc379d95b8be9b71cd6bde4c28728f306811803b","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e1e46d0a9837ee058c100501080c920fa98081ea3956af0374308ba6f22a33e","impliedFormat":1},{"version":"272ca407e0c9068bdc5152552d876e68037ceae3de62e529306403e973dec8e1","impliedFormat":1},{"version":"fa7834c715d5357e4540cee40ce96c3250ddb67a7b879a6b7fa0e86d6696f121","impliedFormat":1},{"version":"22dfb07a7ab15b66ac043829056fe70124844636ae719551812ac631ba04985b","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6f137d651076822d4fe884287e68fd61785a0d3d1fdb250a5059b691fa897db","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"0cb167c371eaa8c869f8a7656a7296f2e4fae43b4d8b803a680236b24794e5f9","impliedFormat":1},{"version":"0a839dba0287cc0481ad4beedd48a1c64acf1e212ae865d1315f7007ca215161","impliedFormat":1},{"version":"38dc4655376cd1a4bd6bb3763d92949233e33d38d3dd3cbea7bbf218175a38ef","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"208a6a0bfb227bdf8fb964799d0a206c75cf03ccd72e63bf5495c9331354c6d6","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"48a679952eefe4cb776d5a0e1ccba2d3eb53b57448bbb7abc1fcebcbd5440188","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2d14da6ecb49bf828d83948765ec2d3a579d476bbb9645e749610baa6ec880ca","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"0aef708fb4c7a6b915e8305cbfac40cd207b032dbaabe9a01889a5fff3254681","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"ad9bdafb4e7abf14cc53ce7970486a84c87831e62891e5dfe798ddcd55e84701","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"71d3ae6a5e73ca4130762560425e00984ebaff64d5353a3333d1bb7eb86ef336","impliedFormat":1},{"version":"fc5ad2ec8b130a59612c5cfcc2cdca9bcdd80851adb8acdb004aaeff00cc978d","impliedFormat":99},{"version":"f76d2226b20ec0d3e884114cfbbb933cb4d25d26cc306e8315e21a0e3afafc39","impliedFormat":99},{"version":"709b4ace1214759ca558d2b85c1ac20011c384077769ed8e08d6b2e6d68ef6d9","impliedFormat":99},{"version":"638a22f8e16b31c37297483fa38cfc760ed309b16d4c606a7b4532f4394c9c0c","impliedFormat":99},{"version":"9394183f4c37b8591156f32e41223c9e0dd211eefe7499155d4cdf15268eaea8","impliedFormat":99},{"version":"2403907e4ef7f180350b25e0acb6ffa78c610047d159ecc6964b4229bfb40fb2","impliedFormat":99},{"version":"1504305fd16da58cd87b9ed49129a10b7fa354f414f539bdfbe403658cd6be8f","impliedFormat":99},{"version":"0824e917b53180b0b433f4fa85a1cc1d689a6c5f118bfdbe2086aa3b328c12db","impliedFormat":99},{"version":"7d3e062a778b8f5ea4f0cac7e925e31f88e6739812ebc5f827474324a4048f14","impliedFormat":99},{"version":"6fd401f9fc270efbde915a01325950b046bbfa4e041c4cde91566b3d178b05fc","impliedFormat":99},{"version":"9327db71e743ca5810396f33be57f88bae2b52d53c00aeb2668f41a6ef14c52a","impliedFormat":99},{"version":"e27361b231a041b6c4da9fae61463d39d99f64926e1cd732513fd413b77bf869","impliedFormat":99},{"version":"07cb5a2736a656d1d53b10f7717d5e816613741510a42dfc32f0f257406900e9","impliedFormat":99},{"version":"1f97a93af87366e65131932b6f5173b8eebfb7c3a61aa32626cc348b51c77b80","impliedFormat":99},{"version":"e0864480ea083087d705f9405bd6bf59b795e8474c3447f0d6413b2bce535a09","impliedFormat":99},{"version":"e67cbea16f1994af89efd700542dbf3828a46a52b29e4d67e801bd7869dc103c","impliedFormat":99},{"version":"d127908bdafc0f2d80a2ad9b17b6599d411545ced7773da2374e75d7992ccef2","impliedFormat":99},{"version":"f582b0fcbf1eea9b318ab92fb89ea9ab2ebb84f9b60af89328a91155e1afce72","impliedFormat":99},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"38cb107048cd8ba54a70014ef9a30cf57bee0d9f10a0ca4cefa974056e1ee460","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"3cc8a3d123b6b232d48d34b51b785f9da8d193f5b5817fa521fcd2f3b9315c55","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"9a195d8476f48523446ece812e5450b5b1ec8c3b6abf99efb8ee2479524f6f56","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"354a7f8e1287d9d6b7561bc97fdd8cbc2f7c1dd79e4cb37b942e8a5cfaff1085","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"d8a62a1763683460cfc7c6d66214859909d3f91f34b6e9009aa9443b241f08c0","affectsGlobalScope":true,"impliedFormat":99},{"version":"f3fe8197217cb6fd6e07c0eaa0966e4ece77cd779cbfc55767a93704cc692e04","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"617112682fcb55805f0a0e16450397edc016881962eee4298f4f4e3ef795aee8","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":99},{"version":"71fd5e58251bf9faae3c6c3413db7fe917abce3cbfb5390daac2d971dca8f68d","impliedFormat":99},{"version":"e50d04d261120f8f9d07e85698735d5b88f3a66bc395c08ce26c3d817d141a73","affectsGlobalScope":true,"impliedFormat":99},{"version":"8ecc9dd98b423db66eefccce5e7fcf5d6fb452d5673fcdbe751cf7977dd93b9f","impliedFormat":99},{"version":"6b50db6951aa5dd210715a37636f6f549ffc21556933ed6ff8a1653305a4c360","impliedFormat":99},{"version":"087049bb778b44ad4e720892e4bb22558e54bd6b51d5d746bd0e688eda35f4d8","impliedFormat":99},{"version":"751764bb94219b4ce8f5475dc35d3de2e432fea01a0c9610cd7f69ad05e398c6","impliedFormat":1},{"version":"58eb22ce690f7941a952f307d51a210195798c9fbc381470fe7e0adb228d4c92","impliedFormat":1},{"version":"f63cb353cd53da6be4a34f6fdece6316dac14fd62cccf9a4d2ce6bab2c37bc8c","impliedFormat":1},{"version":"cace2314ce18e7efedfcdaddb80e6428bb3d1b52a874eaa6bff779ba0c36de2b","impliedFormat":1},{"version":"3d922ac35e7bd201c09c71d0a3be9cab0ac41bdd0d5115f2734c8555629e5414","impliedFormat":1},{"version":"ae18a824baa6829b4b687f4e678d97c2b3f0ee75a82e2cff792180002f1e2a82","impliedFormat":1},{"version":"fe1baccba85e2af0fdaca57b32b34f3fd602609bb0b29aeb0609000dbcd75446","impliedFormat":1},{"version":"b7c5ef46a22a80ddac8f1e794c9b91c7082b515936e7dadb2c537ceb66b28ec5","impliedFormat":99},{"version":"1c7b696b935809569a770386fe99319147ed3d4d67ab9bfa1f35b632380b3bfb","signature":"f1a1b21a223c18a29308ebff0b002317e4bb8aa5e350164f8c8c3b8bde33a535"}],"root":[270],"options":{"allowSyntheticDefaultImports":true,"composite":true,"module":99},"referencedMap":[[215,1],[217,1],[218,2],[263,3],[266,1],[267,4],[268,5],[107,6],[105,1],[262,1],[160,7],[161,7],[162,8],[114,9],[163,10],[164,11],[165,12],[109,1],[112,13],[110,1],[111,1],[166,14],[167,15],[168,16],[169,17],[170,18],[171,19],[172,19],[173,20],[174,21],[175,22],[176,23],[115,1],[113,1],[177,24],[178,25],[179,26],[213,27],[180,28],[181,1],[182,29],[183,30],[184,31],[185,32],[186,33],[187,34],[188,35],[189,36],[190,37],[191,37],[192,38],[193,1],[194,39],[195,40],[197,41],[196,42],[198,43],[199,44],[200,45],[201,46],[202,47],[203,48],[204,49],[205,50],[206,51],[207,52],[208,53],[209,54],[210,55],[116,1],[117,1],[118,1],[156,56],[157,1],[158,1],[159,43],[211,57],[212,58],[261,59],[260,1],[97,60],[98,61],[96,1],[106,1],[102,62],[104,63],[103,64],[101,65],[100,1],[248,66],[246,67],[247,68],[235,69],[236,67],[243,70],[234,71],[239,72],[249,1],[240,73],[245,74],[251,75],[250,76],[233,77],[241,78],[242,79],[237,80],[244,66],[238,81],[264,1],[265,82],[230,83],[257,84],[220,85],[221,86],[224,87],[216,88],[223,89],[219,90],[214,1],[225,91],[226,92],[232,1],[84,1],[80,1],[81,1],[15,1],[13,1],[14,1],[19,1],[18,1],[2,1],[20,1],[21,1],[22,1],[23,1],[24,1],[25,1],[26,1],[27,1],[3,1],[28,1],[29,1],[4,1],[30,1],[34,1],[31,1],[32,1],[33,1],[35,1],[36,1],[37,1],[5,1],[38,1],[39,1],[40,1],[41,1],[6,1],[45,1],[42,1],[43,1],[44,1],[46,1],[7,1],[47,1],[52,1],[53,1],[48,1],[49,1],[50,1],[51,1],[8,1],[57,1],[54,1],[55,1],[56,1],[58,1],[9,1],[59,1],[60,1],[61,1],[63,1],[62,1],[64,1],[65,1],[10,1],[66,1],[67,1],[68,1],[11,1],[69,1],[70,1],[71,1],[72,1],[73,1],[74,1],[12,1],[75,1],[82,1],[76,1],[77,1],[78,1],[79,1],[1,1],[17,1],[16,1],[134,93],[144,94],[133,93],[154,95],[125,96],[124,97],[153,98],[147,99],[152,100],[127,101],[141,102],[126,103],[150,104],[122,105],[121,98],[151,106],[123,107],[128,108],[129,1],[132,108],[119,1],[155,109],[145,110],[136,111],[137,112],[139,113],[135,114],[138,115],[148,98],[130,116],[131,117],[140,118],[120,119],[143,110],[142,108],[146,1],[149,120],[269,121],[87,122],[259,123],[90,124],[253,1],[255,125],[254,1],[88,122],[86,1],[89,126],[258,1],[252,1],[227,1],[228,1],[256,127],[222,128],[231,1],[229,129],[94,130],[85,131],[92,132],[99,133],[91,134],[95,135],[93,136],[108,137],[83,1],[270,138]],"semanticDiagnosticsPerFile":[[266,[{"start":43,"length":13,"code":7016,"category":1,"messageText":{"messageText":"Could not find a declaration file for module '@babel/core'. '/Users/pavel/Desktop/DocsGPT-main/frontend/node_modules/@babel/core/lib/index.js' implicitly has an 'any' type.","category":1,"code":7016,"next":[{"info":{"moduleReference":"@babel/core","mode":99}}]}},{"start":90,"length":17,"code":7016,"category":1,"messageText":{"messageText":"Could not find a declaration file for module '@babel/template'. '/Users/pavel/Desktop/DocsGPT-main/frontend/node_modules/@babel/template/lib/index.js' implicitly has an 'any' type.","category":1,"code":7016,"next":[{"info":{"moduleReference":"@babel/template","mode":99}}]}}]],[267,[{"start":26,"length":13,"code":7016,"category":1,"messageText":{"messageText":"Could not find a declaration file for module '@babel/core'. '/Users/pavel/Desktop/DocsGPT-main/frontend/node_modules/@babel/core/lib/index.js' implicitly has an 'any' type.","category":1,"code":7016,"next":[{"info":{"moduleReference":"@babel/core","mode":99}}]}}]],[268,[{"start":71,"length":6,"messageText":"Cannot find module 'svgo' or its corresponding type declarations.","category":1,"code":2307},{"start":171,"length":13,"code":7016,"category":1,"messageText":{"messageText":"Could not find a declaration file for module '@babel/core'. '/Users/pavel/Desktop/DocsGPT-main/frontend/node_modules/@babel/core/lib/index.js' implicitly has an 'any' type.","category":1,"code":7016,"next":[{"info":{"moduleReference":"@babel/core","mode":99}}]}}]]],"latestChangedDtsFile":"./vite.config.d.ts","version":"6.0.3"} \ No newline at end of file diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo deleted file mode 100644 index bf201af2..00000000 --- a/frontend/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/app.tsx","./src/hero.tsx","./src/navigation.tsx","./src/pagenotfound.tsx","./src/env.ts","./src/main.tsx","./src/store.ts","./src/vite-env.d.ts","./src/admin/activity.tsx","./src/admin/adminui.test.ts","./src/admin/adminui.tsx","./src/admin/admins.tsx","./src/admin/loaderror.test.tsx","./src/admin/overview.tsx","./src/admin/quotaeditor.tsx","./src/admin/quotas.tsx","./src/admin/usage.tsx","./src/admin/usagechart.test.ts","./src/admin/usagechart.tsx","./src/admin/userquotamodal.tsx","./src/admin/userusagemodal.tsx","./src/admin/users.tsx","./src/admin/index.tsx","./src/admin/quotautils.test.ts","./src/admin/quotautils.ts","./src/admin/usagechartdata.test.ts","./src/admin/usagechartdata.ts","./src/agents/agentcard.test.tsx","./src/agents/agentcard.tsx","./src/agents/agentlogs.test.tsx","./src/agents/agentlogs.tsx","./src/agents/agentpageheader.test.tsx","./src/agents/agentpageheader.tsx","./src/agents/agentpreview.tsx","./src/agents/agentrouteguard.test.tsx","./src/agents/agentrouteguard.tsx","./src/agents/agentslist.tsx","./src/agents/foldercard.tsx","./src/agents/newagent.test.tsx","./src/agents/newagent.tsx","./src/agents/sharedagent.tsx","./src/agents/sharedagentcard.test.tsx","./src/agents/sharedagentcard.tsx","./src/agents/sharedagentgate.tsx","./src/agents/agentaccess.test.ts","./src/agents/agentaccess.ts","./src/agents/agentpreviewslice.ts","./src/agents/agents.config.ts","./src/agents/index.tsx","./src/agents/paths.test.ts","./src/agents/paths.ts","./src/agents/components/agentpagetoolbar.test.tsx","./src/agents/components/agentpagetoolbar.tsx","./src/agents/components/agentpreviewsheet.test.tsx","./src/agents/components/agentpreviewsheet.tsx","./src/agents/components/agenttypemodal.test.tsx","./src/agents/components/agenttypemodal.tsx","./src/agents/components/guardrailevents.test.tsx","./src/agents/components/guardrailevents.tsx","./src/agents/components/guardrailssection.test.tsx","./src/agents/components/guardrailssection.tsx","./src/agents/components/sponsoredresourcesnotice.test.tsx","./src/agents/components/sponsoredresourcesnotice.tsx","./src/agents/hooks/useagentsearch.ts","./src/agents/hooks/useagentsfetch.ts","./src/agents/schedules/rundetaildrawer.test.tsx","./src/agents/schedules/rundetaildrawer.tsx","./src/agents/schedules/runlog.test.tsx","./src/agents/schedules/runlog.tsx","./src/agents/schedules/scheduleformmodal.tsx","./src/agents/schedules/schedulerow.test.tsx","./src/agents/schedules/schedulerow.tsx","./src/agents/schedules/schedulertoolcallcard.test.ts","./src/agents/schedules/schedulertoolcallcard.tsx","./src/agents/schedules/schedulesview.test.tsx","./src/agents/schedules/schedulesview.tsx","./src/agents/schedules/statusbadge.test.tsx","./src/agents/schedules/statusbadge.tsx","./src/agents/schedules/timezonecombobox.test.ts","./src/agents/schedules/timezonecombobox.tsx","./src/agents/schedules/cronbuilder.test.ts","./src/agents/schedules/cronbuilder.ts","./src/agents/schedules/schedulesslice.test.ts","./src/agents/schedules/schedulesslice.ts","./src/agents/types/index.ts","./src/agents/types/schedule.ts","./src/agents/types/workflow.ts","./src/agents/workflow/canvascontrols.tsx","./src/agents/workflow/nodepalette.test.tsx","./src/agents/workflow/nodepalette.tsx","./src/agents/workflow/workflowbuilder.tsx","./src/agents/workflow/workflowminimap.test.tsx","./src/agents/workflow/workflowmodelscontext.ts","./src/agents/workflow/workflowpreview.test.tsx","./src/agents/workflow/workflowpreview.tsx","./src/agents/workflow/workflowrunartifacts.test.tsx","./src/agents/workflow/workflowrunartifacts.tsx","./src/agents/workflow/codenodeconfig.test.ts","./src/agents/workflow/codenodeconfig.ts","./src/agents/workflow/documentconfig.test.ts","./src/agents/workflow/documentconfig.ts","./src/agents/workflow/nodetones.test.ts","./src/agents/workflow/nodetones.ts","./src/agents/workflow/simplecel.test.ts","./src/agents/workflow/simplecel.ts","./src/agents/workflow/workflowhelpers.test.ts","./src/agents/workflow/workflowhelpers.ts","./src/agents/workflow/workflowpreviewslice.test.ts","./src/agents/workflow/workflowpreviewslice.ts","./src/agents/workflow/components/mobileblocker.tsx","./src/agents/workflow/components/nodedocumentscontrol.tsx","./src/agents/workflow/components/prompttextarea.test.tsx","./src/agents/workflow/components/prompttextarea.tsx","./src/agents/workflow/components/workflowdetailssheet.test.tsx","./src/agents/workflow/components/workflowdetailssheet.tsx","./src/agents/workflow/hooks/useundoredo.ts","./src/agents/workflow/nodes/basenode.tsx","./src/agents/workflow/nodes/codenode.tsx","./src/agents/workflow/nodes/conditionnode.tsx","./src/agents/workflow/nodes/outputvariableline.tsx","./src/agents/workflow/nodes/setstatenode.tsx","./src/agents/workflow/nodes/index.tsx","./src/agents/workflow/panels/agentpanel.test.tsx","./src/agents/workflow/panels/agentpanel.tsx","./src/agents/workflow/panels/codepanel.tsx","./src/agents/workflow/panels/conditionpanel.test.tsx","./src/agents/workflow/panels/conditionpanel.tsx","./src/agents/workflow/panels/nodepanel.test.tsx","./src/agents/workflow/panels/nodepanel.tsx","./src/agents/workflow/panels/notepanel.tsx","./src/agents/workflow/panels/statepanel.test.tsx","./src/agents/workflow/panels/statepanel.tsx","./src/agents/workflow/panels/types.ts","./src/api/client.ts","./src/api/endpoints.ts","./src/api/throttle.ts","./src/api/services/adminservice.ts","./src/api/services/conversationservice.ts","./src/api/services/custommodelsservice.ts","./src/api/services/devicesservice.ts","./src/api/services/modelservice.ts","./src/api/services/patservice.test.ts","./src/api/services/patservice.ts","./src/api/services/schedulesservice.test.ts","./src/api/services/schedulesservice.ts","./src/api/services/teamsservice.test.ts","./src/api/services/teamsservice.ts","./src/api/services/userservice.ts","./src/components/actionbuttons.tsx","./src/components/adminroute.test.tsx","./src/components/adminroute.tsx","./src/components/artifactsidebar.test.tsx","./src/components/artifactsidebar.tsx","./src/components/chunks.test.tsx","./src/components/chunks.tsx","./src/components/configfields.test.tsx","./src/components/configfields.tsx","./src/components/connectorauth.test.tsx","./src/components/connectorauth.tsx","./src/components/connectortree.test.tsx","./src/components/connectortree.tsx","./src/components/copybutton.test.tsx","./src/components/copybutton.tsx","./src/components/documentartifactview.tsx","./src/components/errorboundary.test.tsx","./src/components/errorboundary.tsx","./src/components/filepicker.test.tsx","./src/components/filepicker.tsx","./src/components/filetree.tsx","./src/components/fileupload.test.tsx","./src/components/fileupload.tsx","./src/components/googledrivepicker.tsx","./src/components/graphview.tsx","./src/components/head.tsx","./src/components/help.tsx","./src/components/markdownpreview.tsx","./src/components/mermaidrenderer.test.tsx","./src/components/mermaidrenderer.tsx","./src/components/messageinput.test.tsx","./src/components/messageinput.tsx","./src/components/multiselectpopover.test.tsx","./src/components/multiselectpopover.tsx","./src/components/notification.tsx","./src/components/pagetoolbar.test.tsx","./src/components/pagetoolbar.tsx","./src/components/profilebutton.tsx","./src/components/rolebadge.test.tsx","./src/components/rolebadge.tsx","./src/components/searchinput.test.tsx","./src/components/searchinput.tsx","./src/components/skeletonloader.test.tsx","./src/components/skeletonloader.tsx","./src/components/sourcemarkdown.test.tsx","./src/components/sourcemarkdown.tsx","./src/components/sourcespopoverfooter.test.tsx","./src/components/sourcespopoverfooter.tsx","./src/components/statcard.test.tsx","./src/components/statcard.tsx","./src/components/texttospeechbutton.tsx","./src/components/toolicon.tsx","./src/components/uploadtoast.test.tsx","./src/components/uploadtoast.tsx","./src/components/viewonlynotice.test.tsx","./src/components/viewonlynotice.tsx","./src/components/wikiviewer.test.tsx","./src/components/wikiviewer.tsx","./src/components/artifactviewutils.test.ts","./src/components/artifactviewutils.ts","./src/components/chunkutils.test.ts","./src/components/chunkutils.ts","./src/components/graphviewutils.test.ts","./src/components/graphviewutils.ts","./src/components/mermaidrenderqueue.test.ts","./src/components/mermaidsecurity.test.ts","./src/components/mermaidsecurity.ts","./src/components/useartifactbytes.ts","./src/components/wikiviewerutils.test.ts","./src/components/wikiviewerutils.ts","./src/components/graph/graphcanvascontrols.tsx","./src/components/graph/graphchunksheet.test.tsx","./src/components/graph/graphchunksheet.tsx","./src/components/graph/graphentities.tsx","./src/components/graph/graphentitysearch.tsx","./src/components/graph/graphnodepanel.test.tsx","./src/components/graph/graphnodepanel.tsx","./src/components/graph/graphsourceview.test.tsx","./src/components/graph/graphsourceview.tsx","./src/components/graph/graphtypedot.tsx","./src/components/graph/graphcanvasutils.test.ts","./src/components/graph/graphcanvasutils.ts","./src/components/graph/usegraphnodedetail.ts","./src/components/message-input/attachfilebutton.tsx","./src/components/message-input/attachmentchiplist.test.tsx","./src/components/message-input/attachmentchiplist.tsx","./src/components/message-input/micbutton.tsx","./src/components/message-input/sourcestrigger.tsx","./src/components/message-input/toolstrigger.tsx","./src/components/message-input/armedsend.test.tsx","./src/components/message-input/armedsend.ts","./src/components/message-input/attachmentreadability.test.ts","./src/components/message-input/attachmentreadability.ts","./src/components/message-input/composercontrols.test.tsx","./src/components/message-input/index.ts","./src/components/message-input/uploadstallguard.test.ts","./src/components/message-input/uploadstallguard.ts","./src/components/tree/pathheader.test.tsx","./src/components/tree/pathheader.tsx","./src/components/tree/readerpanel.tsx","./src/components/tree/sourceeditsheet.test.tsx","./src/components/tree/sourceeditsheet.tsx","./src/components/tree/sourcenavigator.test.tsx","./src/components/tree/sourcenavigator.tsx","./src/components/tree/treebrowser.test.tsx","./src/components/tree/treebrowser.tsx","./src/components/tree/navigatorutils.test.ts","./src/components/tree/navigatorutils.ts","./src/components/tree/types.ts","./src/components/tree/usereingestwait.ts","./src/components/types/index.ts","./src/components/ui/accordion.test.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert.test.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.test.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.test.tsx","./src/components/ui/badge.tsx","./src/components/ui/bar-tint-reset.ts","./src/components/ui/breadcrumb.test.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.test.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.test.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.test.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/command.test.tsx","./src/components/ui/command.tsx","./src/components/ui/description-list.test.tsx","./src/components/ui/description-list.tsx","./src/components/ui/dialog.test.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.test.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/dropzone.test.tsx","./src/components/ui/dropzone.tsx","./src/components/ui/empty-state.test.tsx","./src/components/ui/empty-state.tsx","./src/components/ui/form-field.test.tsx","./src/components/ui/form-field.tsx","./src/components/ui/icon-button.test.tsx","./src/components/ui/icon-button.tsx","./src/components/ui/input.test.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/list-row.test.tsx","./src/components/ui/list-row.tsx","./src/components/ui/loading-state.test.tsx","./src/components/ui/loading-state.tsx","./src/components/ui/message-scroller.tsx","./src/components/ui/modal.test.tsx","./src/components/ui/modal.tsx","./src/components/ui/multi-select.test.tsx","./src/components/ui/multi-select.tsx","./src/components/ui/option-card.test.tsx","./src/components/ui/option-card.tsx","./src/components/ui/overlay-chrome.test.tsx","./src/components/ui/pagination-size.test.tsx","./src/components/ui/pagination.test.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.test.tsx","./src/components/ui/progress.tsx","./src/components/ui/section-header.test.tsx","./src/components/ui/section-header.tsx","./src/components/ui/select.test.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.test.tsx","./src/components/ui/separator.tsx","./src/components/ui/setting-row.test.tsx","./src/components/ui/setting-row.tsx","./src/components/ui/sheet.test.tsx","./src/components/ui/sheet.tsx","./src/components/ui/skeleton.test.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/spinner.test.tsx","./src/components/ui/spinner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.test.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.test.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.test.tsx","./src/components/ui/textarea.tsx","./src/components/ui/time-picker.test.tsx","./src/components/ui/time-picker.tsx","./src/components/ui/toast.test.tsx","./src/components/ui/toast.tsx","./src/components/ui/toggle-group.test.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/tooltip.test.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/use-focus-return.ts","./src/constants/fileupload.test.ts","./src/constants/fileupload.ts","./src/conversation/answerflow.test.tsx","./src/conversation/answerflow.tsx","./src/conversation/conversation.tsx","./src/conversation/conversationbubble.test.tsx","./src/conversation/conversationbubble.tsx","./src/conversation/conversationmessages.test.tsx","./src/conversation/conversationmessages.tsx","./src/conversation/conversationtile.test.tsx","./src/conversation/conversationtile.tsx","./src/conversation/markdownanswer.codespans.test.tsx","./src/conversation/markdownanswer.math.test.tsx","./src/conversation/markdownanswer.sandbox.test.tsx","./src/conversation/markdownanswer.tsx","./src/conversation/researchprogress.tsx","./src/conversation/sharedconversation.tsx","./src/conversation/streamingstatusline.tsx","./src/conversation/wikiwritetoolcallcard.test.tsx","./src/conversation/answerflowintegration.test.tsx","./src/conversation/answersegments.test.ts","./src/conversation/answersegments.ts","./src/conversation/artifactchips.test.ts","./src/conversation/artifactchips.ts","./src/conversation/conversationhandlers.ts","./src/conversation/conversationlistener.test.ts","./src/conversation/conversationmodels.ts","./src/conversation/conversationslice.test.ts","./src/conversation/conversationslice.ts","./src/conversation/quotaerror.test.ts","./src/conversation/quotaerror.ts","./src/conversation/sandboxlinks.live.test.ts","./src/conversation/sandboxlinks.test.ts","./src/conversation/sandboxlinks.ts","./src/conversation/sharedconversationslice.ts","./src/conversation/wikitoolcall.test.ts","./src/conversation/wikitoolcall.ts","./src/conversation/markdown/answermarkdown.ts","./src/conversation/markdown/micromarkextension.ts","./src/conversation/markdown/remarkcitations.ts","./src/conversation/markdown/remarkdisplaymath.ts","./src/conversation/markdown/singledollarmath.ts","./src/conversation/markdown/texmath.ts","./src/conversation/types/index.ts","./src/design/designsystem.tsx","./src/design/cardsurfaces.lint.test.ts","./src/design/designrules.lint.test.ts","./src/events/eventstreamprovider.tsx","./src/events/dispatchevent.test.ts","./src/events/dispatchevent.ts","./src/events/eventstreamclient.ts","./src/events/useeventstream.ts","./src/hooks/fetchmeroles.test.ts","./src/hooks/index.ts","./src/hooks/usedarktheme.test.tsx","./src/hooks/usedatainitializer.ts","./src/hooks/usemediaquery.test.tsx","./src/hooks/usetokenauth.ts","./src/lib/markdown.test.tsx","./src/lib/markdown.tsx","./src/lib/utils.test.ts","./src/lib/utils.ts","./src/locale/accesstokens.test.ts","./src/locale/i18n.ts","./src/locale/logstrace.test.ts","./src/modals/accesstokencreatedmodal.tsx","./src/modals/addactionmodal.tsx","./src/modals/addtoolmodal.tsx","./src/modals/agentdetailsmodal.test.tsx","./src/modals/agentdetailsmodal.tsx","./src/modals/configtoolmodal.tsx","./src/modals/confirmationmodal.tsx","./src/modals/createaccesstokenmodal.tsx","./src/modals/custommodelmodal.test.tsx","./src/modals/custommodelmodal.tsx","./src/modals/foldermanagementmodal.tsx","./src/modals/importagentmodal.test.tsx","./src/modals/importagentmodal.tsx","./src/modals/importspecmodal.tsx","./src/modals/jwtmodal.tsx","./src/modals/mcpservermodal.test.tsx","./src/modals/mcpservermodal.tsx","./src/modals/movetofoldermodal.test.tsx","./src/modals/movetofoldermodal.tsx","./src/modals/regenerateaccesstokenmodal.tsx","./src/modals/searchconversationsmodal.test.tsx","./src/modals/searchconversationsmodal.tsx","./src/modals/shareconversationmodal.tsx","./src/modals/types/index.ts","./src/models/misc.ts","./src/models/types.ts","./src/navigation/detailbreadcrumb.tsx","./src/navigation/mobiletopbar.test.tsx","./src/navigation/mobiletopbar.tsx","./src/navigation/sectionindexpage.tsx","./src/navigation/sectionnav.tsx","./src/navigation/sectionpageheader.tsx","./src/navigation/sectionpills.tsx","./src/navigation/sectionrail.tsx","./src/navigation/sectionshell.test.tsx","./src/navigation/sectionshell.tsx","./src/navigation/sidebarlevel.tsx","./src/navigation/sidebarlevelprovider.tsx","./src/navigation/sidebarnavrow.test.tsx","./src/navigation/sections.test.ts","./src/navigation/sections.ts","./src/navigation/uselastapppath.ts","./src/navigation/usesectioncontext.ts","./src/navigation/usesectionresolver.ts","./src/notifications/actiontoast.test.tsx","./src/notifications/actiontoast.tsx","./src/notifications/teamnotificationtoast.test.tsx","./src/notifications/teamnotificationtoast.tsx","./src/notifications/toolapprovaltoast.test.tsx","./src/notifications/toolapprovaltoast.tsx","./src/notifications/actiontoastslice.test.ts","./src/notifications/actiontoastslice.ts","./src/notifications/dismissedpersistence.test.ts","./src/notifications/dismissedpersistence.ts","./src/notifications/notificationsslice.test.ts","./src/notifications/notificationsslice.ts","./src/preferences/promptsmodal.test.tsx","./src/preferences/promptsmodal.tsx","./src/preferences/preferenceapi.ts","./src/preferences/preferenceslice.test.ts","./src/preferences/preferenceslice.ts","./src/preferences/types/index.ts","./src/settings/analytics.tsx","./src/settings/converttowikimodal.tsx","./src/settings/custommodels.tsx","./src/settings/enablegraphragmodal.tsx","./src/settings/general.tsx","./src/settings/logs.tsx","./src/settings/pairdevicemodal.tsx","./src/settings/personalaccesstokens.tsx","./src/settings/prompts.test.tsx","./src/settings/prompts.tsx","./src/settings/remotedeviceconfig.test.tsx","./src/settings/remotedeviceconfig.tsx","./src/settings/sourceconfigmodal.test.tsx","./src/settings/sourceconfigmodal.tsx","./src/settings/sources.test.tsx","./src/settings/sources.tsx","./src/settings/teams.test.tsx","./src/settings/teams.tsx","./src/settings/testretrievalmodal.notices.test.tsx","./src/settings/testretrievalmodal.test.tsx","./src/settings/testretrievalmodal.tsx","./src/settings/toolconfig.test.tsx","./src/settings/toolconfig.tsx","./src/settings/tools.test.tsx","./src/settings/tools.tsx","./src/settings/accesstokenutils.test.ts","./src/settings/accesstokenutils.ts","./src/settings/foldseries.test.ts","./src/settings/foldseries.ts","./src/settings/graphbuildslice.test.ts","./src/settings/graphbuildslice.ts","./src/settings/graphragenableutils.test.ts","./src/settings/graphragenableutils.ts","./src/settings/index.test.tsx","./src/settings/index.tsx","./src/settings/wikiconvertutils.test.ts","./src/settings/wikiconvertutils.ts","./src/settings/components/retrievaloptions.test.tsx","./src/settings/components/retrievaloptions.tsx","./src/settings/components/usagequota.tsx","./src/settings/traces/tracechips.tsx","./src/settings/traces/tracesheet.test.tsx","./src/settings/traces/tracesheet.tsx","./src/settings/traces/tracespandetails.tsx","./src/settings/traces/tracewaterfall.tsx","./src/settings/traces/traceutils.test.ts","./src/settings/traces/traceutils.ts","./src/settings/types/index.ts","./src/teams/sharetoteammodal.test.tsx","./src/teams/sharetoteammodal.tsx","./src/teams/teamswitcher.tsx","./src/teams/accesssettings.ts","./src/teams/teamsslice.test.ts","./src/teams/teamsslice.ts","./src/upload/upload.test.tsx","./src/upload/upload.tsx","./src/upload/uploadslice.test.ts","./src/upload/uploadslice.ts","./src/upload/types/ingestor.test.ts","./src/upload/types/ingestor.ts","./src/utils/accessutils.test.ts","./src/utils/accessutils.ts","./src/utils/browserutils.ts","./src/utils/chartutils.test.ts","./src/utils/chartutils.ts","./src/utils/connectorauthutils.test.ts","./src/utils/connectorauthutils.ts","./src/utils/datetimeutils.test.ts","./src/utils/datetimeutils.ts","./src/utils/httpmethodcolors.test.ts","./src/utils/httpmethodcolors.ts","./src/utils/idempotency.ts","./src/utils/jwtutils.test.ts","./src/utils/jwtutils.ts","./src/utils/objectutils.ts","./src/utils/providerutils.ts","./src/utils/sourceutils.test.ts","./src/utils/sourceutils.ts","./src/utils/streamingstatusutils.test.ts","./src/utils/streamingstatusutils.ts","./src/utils/stringutils.ts","./src/utils/toolutils.test.ts","./src/utils/toolutils.ts"],"version":"6.0.3"} \ No newline at end of file diff --git a/frontend/vite.config.d.ts b/frontend/vite.config.d.ts deleted file mode 100644 index 089eeef9..00000000 --- a/frontend/vite.config.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const _default: import("vite").UserConfigFnObject; -export default _default; diff --git a/frontend/vite.config.js b/frontend/vite.config.js deleted file mode 100644 index 905b0af3..00000000 --- a/frontend/vite.config.js +++ /dev/null @@ -1,41 +0,0 @@ -/// -import { defineConfig, loadEnv } from 'vite'; -import react from '@vitejs/plugin-react'; -import svgr from 'vite-plugin-svgr'; -import path from 'path'; -// https://vitejs.dev/config/ -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ''); - return { - plugins: [react(), svgr()], - resolve: { - alias: { - '@': path.resolve(import.meta.dirname, './src'), - }, - // Radix keeps its body pointer-events lock in module scope. Any second copy - // npm nests, now or after a future bump, can leave that lock stuck on . - dedupe: ['@radix-ui/react-dismissable-layer'], - }, - server: { - // Extra dev hosts (e.g. a tailscale name) come from VITE_ALLOWED_HOSTS in - // an untracked .env.local; machine-specific names stay out of the repo. - allowedHosts: env.VITE_ALLOWED_HOSTS - ? env.VITE_ALLOWED_HOSTS.split(',') - .map((h) => h.trim()) - .filter(Boolean) - : [], - // Use polling for file watching when running inside Docker. - // Native fs events do not propagate from Windows hosts into Linux - // containers, so Chokidar falls back to polling which works reliably. - watch: env.DOCKER - ? { usePolling: true, interval: 300 } - : undefined, - }, - test: { - environment: 'happy-dom', - globals: true, - include: ['src/**/*.test.{ts,tsx}'], - setupFiles: ['./vitest.setup.ts'], - }, - }; -}); diff --git a/tests/api/user/agents/test_roles_access.py b/tests/api/user/agents/test_roles_access.py index 80ddb1aa..022e7130 100644 --- a/tests/api/user/agents/test_roles_access.py +++ b/tests/api/user/agents/test_roles_access.py @@ -246,14 +246,22 @@ class TestUpdateAgent: {"name": "n", "folder_id": str(folder["id"])}) assert _status(resp) == 200 - def test_workflow_validated_against_owner(self, app, pg_conn): + def test_workflow_validated_against_owner_and_owner_only_to_change(self, app, pg_conn): from docsgpt.storage.db.repositories.workflows import WorkflowsRepository wf = WorkflowsRepository(pg_conn).create(OWNER, "wf") agent_id = _agent(pg_conn, agent_type="workflow") + # An editor can't point the agent at another of the owner's workflows. resp = self._put(app, pg_conn, agent_id, EDITOR, {"workflow": str(wf["id"])}) + assert _status(resp) == 403 + assert _row(pg_conn, agent_id)["workflow_id"] is None + + resp = self._put(app, pg_conn, agent_id, OWNER, {"workflow": str(wf["id"])}) assert _status(resp) == 200 assert str(_row(pg_conn, agent_id)["workflow_id"]) == str(wf["id"]) + # Re-sending the current one is a plain save. + resp = self._put(app, pg_conn, agent_id, EDITOR, {"workflow": str(wf["id"])}) + assert _status(resp) == 200 mine = WorkflowsRepository(pg_conn).create(EDITOR, "editor-wf") resp = self._put(app, pg_conn, agent_id, EDITOR, {"workflow": str(mine["id"])}) @@ -269,10 +277,14 @@ class TestUpdateAgent: _team_share(pg_conn, "tool", shared_tool, owner=STRANGER) agent_id = _agent(pg_conn) - ok = self._put(app, pg_conn, agent_id, EDITOR, {"tools": [owner_tool, shared_tool]}) - assert _status(ok) == 200 + # The owner owning a tool isn't enough: it must reach the editor. + denied = self._put(app, pg_conn, agent_id, EDITOR, {"tools": [owner_tool]}) + assert _status(denied) == 403 denied = self._put(app, pg_conn, agent_id, EDITOR, {"tools": [foreign_tool]}) assert _status(denied) == 403 + assert _status(self._put(app, pg_conn, agent_id, OWNER, {"tools": [owner_tool]})) == 200 + ok = self._put(app, pg_conn, agent_id, EDITOR, {"tools": [owner_tool, shared_tool]}) + assert _status(ok) == 200 set_settings(pg_conn, "tool", shared_tool, {"viewers_can_use_in_agents": False}, STRANGER) # Already attached: keeping it is fine. diff --git a/tests/api/user/test_attach_owner_refs.py b/tests/api/user/test_attach_owner_refs.py new file mode 100644 index 00000000..dad9276a --- /dev/null +++ b/tests/api/user/test_attach_owner_refs.py @@ -0,0 +1,212 @@ +"""What a team editor may newly reference from the owner's agent or workflow. + +An agent (and its workflow) runs as its owner, so owning a resource is not +enough for an editor to wire it in: the editor must be able to use it +themselves. Otherwise an editor of one shared agent could attach the owner's +private tool (run with the owner's credentials), source or prompt, or the +workflow of another of the owner's agents and then edit that graph. + +Also covers id casing: an uppercase UUID must resolve the same switches as +the canonical lowercase one. Uses real repositories on ``pg_conn``. +""" + +from __future__ import annotations + +import uuid + +import pytest + +from docsgpt.api.user.resource_access import resolve, set_settings +from docsgpt.storage.db.repositories.agents import AgentsRepository +from docsgpt.storage.db.repositories.prompts import PromptsRepository +from docsgpt.storage.db.repositories.sources import SourcesRepository +from docsgpt.storage.db.repositories.team_resource_grants import ( + TeamResourceGrantsRepository, +) +from docsgpt.storage.db.repositories.user_tools import UserToolsRepository +from docsgpt.storage.db.repositories.workflows import WorkflowsRepository + +from tests.api.user.test_resource_sponsors import ( + EDITOR, + OWNER, + _agent, + _call, + _put, + _row, + _status, + _wf_body, +) + + +@pytest.fixture +def app(): + from flask import Flask + + return Flask(__name__) + + +def _owner_private(conn): + """A tool, prompt and source the owner never shared.""" + tool = str(UserToolsRepository(conn).create(OWNER, "api_tool")["id"]) + prompt = str(PromptsRepository(conn).create(OWNER, "private", "Owner prompt")["id"]) + source = str(SourcesRepository(conn).create("owner-src", user_id=OWNER)["id"]) + return tool, prompt, source + + +def _share_tool_with_editor(conn, team_id, tool, level="viewer"): + TeamResourceGrantsRepository(conn).grant( + team_id, "tool", tool, OWNER, OWNER, access_level=level, target_user_id=EDITOR + ) + + +class TestAgentAttach: + def test_editor_cannot_attach_owner_private_tool(self, app, pg_conn): + agent_id, _ = _agent(pg_conn) + tool, _, _ = _owner_private(pg_conn) + resp = _put(app, pg_conn, agent_id, EDITOR, {"tools": [tool]}) + assert _status(resp) == 403 + assert _row(pg_conn, agent_id)["tools"] in (None, []) + + def test_editor_cannot_attach_owner_private_source(self, app, pg_conn): + agent_id, _ = _agent(pg_conn) + _, _, source = _owner_private(pg_conn) + resp = _put(app, pg_conn, agent_id, EDITOR, {"sources": [source]}) + assert _status(resp) == 403 + + def test_editor_cannot_attach_owner_private_prompt(self, app, pg_conn): + agent_id, _ = _agent(pg_conn) + _, prompt, _ = _owner_private(pg_conn) + resp = _put(app, pg_conn, agent_id, EDITOR, {"prompt_id": prompt}) + assert _status(resp) == 403 + + def test_editor_may_attach_owner_tool_shared_with_them(self, app, pg_conn): + agent_id, team_id = _agent(pg_conn) + tool, _, _ = _owner_private(pg_conn) + _share_tool_with_editor(pg_conn, team_id, tool) + resp = _put(app, pg_conn, agent_id, EDITOR, {"tools": [tool]}) + assert _status(resp) == 200, resp.get_json() + row = _row(pg_conn, agent_id) + assert [str(t) for t in row["tools"]] == [tool] + # The owner owns it, so it runs as the owner: no sponsor. + assert not row.get("resource_sponsors") + + def test_editor_keeps_owner_refs_already_on_agent(self, app, pg_conn): + tool, prompt, source = _owner_private(pg_conn) + agent_id, _ = _agent(pg_conn, tools=[tool], prompt_id=prompt, source_id=source) + resp = _put( + app, pg_conn, agent_id, EDITOR, + {"tools": [tool], "prompt_id": prompt, "source": source}, + ) + assert _status(resp) == 200, resp.get_json() + + def test_owner_may_attach_own_private_tool(self, app, pg_conn): + agent_id, _ = _agent(pg_conn) + tool, _, _ = _owner_private(pg_conn) + assert _status(_put(app, pg_conn, agent_id, OWNER, {"tools": [tool]})) == 200 + + +class TestAgentWorkflowSwap: + def _two_workflow_agents(self, pg_conn): + """Agent A (shared with EDITOR) and the owner's private agent B, each with a workflow.""" + wf_a = str(WorkflowsRepository(pg_conn).create(OWNER, "A")["id"]) + wf_b = str(WorkflowsRepository(pg_conn).create(OWNER, "B")["id"]) + agent_a, _ = _agent(pg_conn, agent_type="workflow", workflow_id=wf_a) + AgentsRepository(pg_conn).create( + OWNER, "Private B", "published", description="d", key=f"k-{uuid.uuid4().hex}", + agent_type="workflow", workflow_id=wf_b, + ) + return agent_a, wf_a, wf_b + + def test_editor_cannot_swap_in_another_owner_workflow(self, app, pg_conn): + agent_a, wf_a, wf_b = self._two_workflow_agents(pg_conn) + resp = _put(app, pg_conn, agent_a, EDITOR, {"workflow": wf_b}) + assert _status(resp) == 403 + assert str(_row(pg_conn, agent_a)["workflow_id"]) == wf_a + + def test_editor_may_resend_current_workflow(self, app, pg_conn): + agent_a, wf_a, _ = self._two_workflow_agents(pg_conn) + resp = _put(app, pg_conn, agent_a, EDITOR, {"workflow": wf_a}) + assert _status(resp) == 200, resp.get_json() + + def test_owner_may_swap_workflow(self, app, pg_conn): + agent_a, _, wf_b = self._two_workflow_agents(pg_conn) + assert _status(_put(app, pg_conn, agent_a, OWNER, {"workflow": wf_b})) == 200 + assert str(_row(pg_conn, agent_a)["workflow_id"]) == wf_b + + +class TestWorkflowNodeAttach: + def _setup(self, pg_conn): + wf = WorkflowsRepository(pg_conn).create(OWNER, "wf") + _, team_id = _agent(pg_conn, agent_type="workflow", workflow_id=str(wf["id"])) + return str(wf["id"]), team_id + + def _put_wf(self, app, pg_conn, wid, user, body): + from docsgpt.api.user.workflows.routes import WorkflowDetail + + return _call(app, pg_conn, WorkflowDetail, "put", f"/api/workflows/{wid}", user, + json=body, args=(wid,)) + + def test_editor_cannot_add_owner_private_tool_to_node(self, app, pg_conn): + wid, _ = self._setup(pg_conn) + tool, _, _ = _owner_private(pg_conn) + assert _status(self._put_wf(app, pg_conn, wid, EDITOR, _wf_body(tool=tool))) == 403 + + def test_editor_cannot_add_owner_private_source_to_node(self, app, pg_conn): + wid, _ = self._setup(pg_conn) + _, _, source = _owner_private(pg_conn) + assert _status(self._put_wf(app, pg_conn, wid, EDITOR, _wf_body(source=source))) == 403 + + def test_editor_keeps_owner_node_refs_already_in_graph(self, app, pg_conn): + wid, _ = self._setup(pg_conn) + tool, _, source = _owner_private(pg_conn) + body = _wf_body(tool=tool, source=source) + assert _status(self._put_wf(app, pg_conn, wid, OWNER, body)) == 200 + resp = self._put_wf(app, pg_conn, wid, EDITOR, body) + assert _status(resp) == 200, resp.get_json() + + def test_editor_may_add_tool_shared_with_them(self, app, pg_conn): + wid, team_id = self._setup(pg_conn) + tool, _, _ = _owner_private(pg_conn) + _share_tool_with_editor(pg_conn, team_id, tool) + resp = self._put_wf(app, pg_conn, wid, EDITOR, _wf_body(tool=tool)) + assert _status(resp) == 200, resp.get_json() + + +class TestUppercaseIds: + def _tool_not_usable_in_own(self, pg_conn, team_id): + """An owner tool the EDITOR only views, with ``viewers_can_use_in_agents`` off.""" + tool, _, _ = _owner_private(pg_conn) + _share_tool_with_editor(pg_conn, team_id, tool) + set_settings(pg_conn, "tool", tool, {"viewers_can_use_in_agents": False}, OWNER) + return tool + + def test_resolve_applies_switches_to_uppercase_id(self, pg_conn): + _, team_id = _agent(pg_conn) + tool = self._tool_not_usable_in_own(pg_conn, team_id) + lower = resolve(pg_conn, "tool", tool, EDITOR) + upper = resolve(pg_conn, "tool", tool.upper(), EDITOR) + assert lower is not None and upper is not None + assert not upper.can("use_in_own") + assert upper.settings == lower.settings + assert upper.resource_id == tool + + def test_uppercase_tool_id_does_not_bypass_switch_on_attach(self, app, pg_conn): + agent_id, team_id = _agent(pg_conn) + tool = self._tool_not_usable_in_own(pg_conn, team_id) + resp = _put(app, pg_conn, agent_id, EDITOR, {"tools": [tool.upper()]}) + assert _status(resp) == 403 + + def test_attached_ids_are_stored_canonical(self, app, pg_conn): + agent_id, _ = _agent(pg_conn) + tool, prompt, source = _owner_private(pg_conn) + resp = _put( + app, pg_conn, agent_id, OWNER, + {"tools": [tool.upper()], "prompt_id": prompt.upper(), "sources": [source.upper()]}, + ) + assert _status(resp) == 200, resp.get_json() + row = _row(pg_conn, agent_id) + assert [str(t) for t in row["tools"]] == [tool] + assert str(row["prompt_id"]) == prompt + assert [str(s) for s in row["extra_source_ids"] or []] + ( + [str(row["source_id"])] if row.get("source_id") else [] + ) == [source] diff --git a/tests/api/user/test_tools_access.py b/tests/api/user/test_tools_access.py index 07d6f3c3..b404f0e0 100644 --- a/tests/api/user/test_tools_access.py +++ b/tests/api/user/test_tools_access.py @@ -503,17 +503,44 @@ class TestMCPSaveAccess: resp, _ = self._save(app, pg_conn, "ed", body) assert resp.status_code == 403 - def test_editor_oauth_edit_without_reconnect_keeps_actions(self, app, pg_conn): + def test_editor_oauth_save_without_reconnect_is_owner_only(self, app, pg_conn): + # A shared OAuth server's connection is the owner's: an editor can't + # save it, even with the same URL (the path, scopes or client could + # still change while reusing the owner's tokens). tool = _mcp_tool(pg_conn, auth_type="oauth") _share(pg_conn, tool["id"], "ed", "editor") body = {"id": str(tool["id"]), "displayName": "Renamed", "config": {"server_url": "https://mcp.example.com/mcp", "auth_type": "oauth", "transport_type": "http"}} resp, _ = self._save(app, pg_conn, "ed", body) - assert resp.status_code == 200, resp.json - row = _row(pg_conn, tool["id"]) - assert row["display_name"] == "Renamed" - assert [a["name"] for a in row["actions"]] == ["old"] + assert resp.status_code == 403 + assert _row(pg_conn, tool["id"])["display_name"] == "M" + + def test_editor_cannot_switch_oauth_server_to_other_auth(self, app, pg_conn): + tool = _mcp_tool(pg_conn, auth_type="oauth") + _share(pg_conn, tool["id"], "ed", "editor") + body = {"id": str(tool["id"]), "displayName": "M", + "config": {"server_url": "https://mcp.example.com/mcp", "auth_type": "bearer", + "transport_type": "http", "bearer_token": "mine"}} + assert self._save(app, pg_conn, "ed", body)[0].status_code == 403 + assert _row(pg_conn, tool["id"])["config"]["auth_type"] == "oauth" + + def test_editor_cannot_switch_server_to_oauth(self, app, pg_conn): + tool = _mcp_tool(pg_conn, secrets={"bearer_token": "tok"}) + _share(pg_conn, tool["id"], "ed", "editor") + body = {"id": str(tool["id"]), "displayName": "M", + "config": {"server_url": "https://mcp.example.com/mcp", "auth_type": "oauth", + "transport_type": "http"}} + assert self._save(app, pg_conn, "ed", body)[0].status_code == 403 + + def test_owner_oauth_save_needs_a_completed_sign_in(self, app, pg_conn): + tool = _mcp_tool(pg_conn, auth_type="oauth") + body = {"id": str(tool["id"]), "displayName": "Renamed", + "config": {"server_url": "https://mcp.example.com/mcp", "auth_type": "oauth", + "transport_type": "http"}} + resp, _ = self._save(app, pg_conn, OWNER, body) + assert resp.status_code == 400 + assert _row(pg_conn, tool["id"])["display_name"] == "M" def test_auth_status_includes_team_mcp_tools(self, app, pg_conn): from docsgpt.api.user.tools.mcp import MCPAuthStatus @@ -562,6 +589,12 @@ class TestMCPTestEndpointAccess: assert self._test(app, pg_conn, "vi", self._body(tool))[0].status_code == 403 assert self._test(app, pg_conn, "eve", self._body(tool))[0].status_code == 404 + def test_editor_test_of_stored_oauth_server_is_owner_only(self, app, pg_conn): + tool = _mcp_tool(pg_conn, auth_type="oauth") + _share(pg_conn, tool["id"], "ed", "editor") + resp, cls = self._test(app, pg_conn, "ed", self._body(tool, bearer_token="mine")) + assert resp.status_code == 403 and not cls.called + def test_editor_oauth_test_is_owner_only(self, app, pg_conn): tool = _mcp_tool(pg_conn, auth_type="oauth") _share(pg_conn, tool["id"], "ed", "editor") @@ -569,3 +602,62 @@ class TestMCPTestEndpointAccess: "auth_type": "oauth", "transport_type": "http"}} resp, cls = self._test(app, pg_conn, "ed", body) assert resp.status_code == 403 and not cls.called + + +# --------------------------------------------------------------------------- +# 6. Shared OAuth MCP servers: the connection is the owner's +# --------------------------------------------------------------------------- +class TestSharedOAuthMCPConfig: + """``/api/update_tool`` and ``/api/update_tool_config`` can't move a shared + OAuth server or switch a shared server to OAuth: OAuth tokens are looked up + by owner + server URL, so either would run on the owner's sign-in + elsewhere. Connection changes on OAuth servers are the owner's.""" + + OTHER = {"server_url": "https://other-mcp.example.org/mcp", "auth_type": "oauth"} + + def _oauth_tool(self, conn): + tool = _mcp_tool(conn, auth_type="oauth") + _share(conn, tool["id"], "ed", "editor") + return tool + + def test_update_tool_config_cannot_move_oauth_server(self, app, pg_conn): + from docsgpt.api.user.tools.routes import UpdateToolConfig + + tool = self._oauth_tool(pg_conn) + body = {"id": str(tool["id"]), "config": dict(self.OTHER)} + assert _call(app, pg_conn, UpdateToolConfig, "ed", json=body).status_code == 403 + assert _row(pg_conn, tool["id"])["config"]["server_url"] == "https://mcp.example.com/mcp" + + def test_update_tool_cannot_move_oauth_server(self, app, pg_conn): + from docsgpt.api.user.tools.routes import UpdateTool + + tool = self._oauth_tool(pg_conn) + body = {"id": str(tool["id"]), "config": dict(self.OTHER)} + assert _call(app, pg_conn, UpdateTool, "ed", json=body).status_code == 403 + assert _row(pg_conn, tool["id"])["config"]["server_url"] == "https://mcp.example.com/mcp" + + def test_update_tool_config_cannot_switch_server_to_oauth(self, app, pg_conn): + from docsgpt.api.user.tools.routes import UpdateToolConfig + + tool = _mcp_tool(pg_conn, secrets={"bearer_token": "tok"}) + _share(pg_conn, tool["id"], "ed", "editor") + body = {"id": str(tool["id"]), + "config": {"server_url": "https://mcp.example.com/mcp", "auth_type": "oauth"}} + assert _call(app, pg_conn, UpdateToolConfig, "ed", json=body).status_code == 403 + assert _row(pg_conn, tool["id"])["config"]["auth_type"] == "bearer" + + def test_editor_still_renames_oauth_server_without_config(self, app, pg_conn): + from docsgpt.api.user.tools.routes import UpdateTool + + tool = self._oauth_tool(pg_conn) + body = {"id": str(tool["id"]), "customName": "Renamed"} + assert _call(app, pg_conn, UpdateTool, "ed", json=body).status_code == 200 + assert _row(pg_conn, tool["id"])["custom_name"] == "Renamed" + + def test_owner_may_change_oauth_server_config(self, app, pg_conn): + from docsgpt.api.user.tools.routes import UpdateToolConfig + + tool = _mcp_tool(pg_conn, auth_type="oauth") + body = {"id": str(tool["id"]), "config": dict(self.OTHER)} + assert _call(app, pg_conn, UpdateToolConfig, OWNER, json=body).status_code == 200 + assert _row(pg_conn, tool["id"])["config"]["server_url"] == self.OTHER["server_url"]