diff --git a/backend/app/services/agent_runtime.py b/backend/app/services/agent_runtime.py index 8ef65ee..1e54e39 100644 --- a/backend/app/services/agent_runtime.py +++ b/backend/app/services/agent_runtime.py @@ -114,8 +114,8 @@ async def run(self, db, model_type, name: str, args: dict, ) return f"权限不足:工具 {name} 需 {allowed_roles} 角色", True tool_args = dict(args) if isinstance(args, dict) else {} - # tenant 是运行时保留参数,LLM/用户参数无权指定或覆盖。 - for reserved in ("tenant", "tenant_id", "tenantId"): + # tenant/creator 是运行时保留参数,LLM/用户参数无权指定或覆盖。 + for reserved in ("tenant", "tenant_id", "tenantId", "creator"): tool_args.pop(reserved, None) if ctx is not None: tenant = str(ctx.get("tenant") or "default").strip() or "default" @@ -131,6 +131,17 @@ async def run(self, db, model_type, name: str, args: dict, if not tenant_aware: return f"工具 {name} 未声明租户隔离能力,已拒绝调用", True tool_args["tenant"] = tenant + # creator 同理取登录态:防 LLM 幻觉/注入伪造开票人(如 _t_create_ticket)。 + username = str(ctx.get("username") or "").strip() + if username: + try: + creator_aware = any( + parameter.name == "creator" for parameter in parameters + ) + except (TypeError, ValueError): + creator_aware = False + if creator_aware: + tool_args["creator"] = username error = False _tool_t0 = time.perf_counter() try: diff --git a/tests/test_agent_tools.py b/tests/test_agent_tools.py index 7224cdb..f434c67 100644 --- a/tests/test_agent_tools.py +++ b/tests/test_agent_tools.py @@ -76,3 +76,49 @@ def test_registry_flag_on(monkeypatch): reg = svc.build_default_registry() assert reg.get("create_ticket") is not None assert reg.get("submit_ticket") is not None + + +def test_creator_reserved_injected_from_ctx(monkeypatch): + """creator 是运行时保留参数:LLM args 里的 creator 被剥掉,注入登录态用户名。""" + from app.services.agent_runtime import Tool, ToolRegistry + + seen = {} + + async def fake_audit(*args, **kwargs): + return None + + async def handler(db, model_type, task="", tenant=None, creator=""): + seen.update(creator=creator, tenant=tenant) + return "ok" + + monkeypatch.setattr( + "app.services.agent_tool_audit_service.log_tool_call", fake_audit, + ) + registry = ToolRegistry() + registry.register(Tool("fake_create", "d", {}, handler)) + result, error = asyncio.run(registry.run( + None, None, "fake_create", + {"task": "x", "creator": "spoofed"}, + ctx={"username": "alice", "tenant": "t1"}, + )) + assert error is False and result == "ok" + assert seen == {"creator": "alice", "tenant": "t1"} + + +def test_creator_stripped_without_ctx(monkeypatch): + """无登录态(老链路 ctx=None):creator 一律剥掉,落 handler 默认空串。""" + from app.services.agent_runtime import Tool, ToolRegistry + + seen = {} + + async def handler(db, model_type, task="", creator=""): + seen.update(creator=creator) + return "ok" + + registry = ToolRegistry() + registry.register(Tool("fake_create", "d", {}, handler)) + result, error = asyncio.run(registry.run( + None, None, "fake_create", {"task": "x", "creator": "spoofed"}, + )) + assert error is False and result == "ok" + assert seen == {"creator": ""}