From 1d6152fb82044fb31142d623d249e13f90f079a5 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Sun, 23 Aug 2026 15:04:51 +0200 Subject: [PATCH] fix(arch-001,arch-002): permissions before on_activate; activate once per process --- app/main.py | 17 +++++++++-------- app/plugins/registry.py | 16 ++++++++++++++++ tests/test_phase_f_agents.py | 6 +++--- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/main.py b/app/main.py index 7aa0c6b..d875b20 100644 --- a/app/main.py +++ b/app/main.py @@ -284,22 +284,23 @@ async def lifespan(app: FastAPI): logger.info(f"Plugin {name} is inactive — skipping activation") continue - # Activate plugin with a FRESH session per plugin to avoid RLS state leakage - # RLS fail-closed requires app.current_tenant_id for tenant-table writes. - # Plugin activation may fail on duplicate cron job inserts — this is harmless - # since cron jobs already exist from previous startups. + # Activate plugin ONCE per process (ARCH-002 fix): a fresh session with + # the first tenant's RLS context satisfies fail-closed RLS for any + # tenant-table writes during activation. Plugins that need per-tenant + # data must seed it themselves (e.g. via the default-tenant mechanism). + # Calling on_activate once prevents duplicate event listeners, cron + # jobs, mini-apps and other contributions at multi-tenant startups. plugin_activated = False - for tenant_id in all_tenant_ids: + if all_tenant_ids: try: async with async_session() as plugin_db: - await set_tenant_context(plugin_db, tenant_id) + await set_tenant_context(plugin_db, all_tenant_ids[0]) await plugin.on_activate(plugin_db, container, event_bus) await plugin_db.flush() await plugin_db.commit() plugin_activated = True except Exception as exc: - logger.warning(f"[STARTUP] Plugin {name} activation issue for tenant {tenant_id}: {exc}") - break + logger.warning(f"[STARTUP] Plugin {name} activation issue: {exc}") if plugin_activated: plugin_record.status = "active" diff --git a/app/plugins/registry.py b/app/plugins/registry.py index db2118e..d503584 100644 --- a/app/plugins/registry.py +++ b/app/plugins/registry.py @@ -608,6 +608,22 @@ class PluginRegistry: for warning in perm_warnings: logger.warning(warning) + # Register permissions and entity models BEFORE on_activate (ARCH-001 fix): + # the activation hook may already rely on its own permissions/entities being + # resolvable (e.g. tools declaring required_permission). + from app.core.permission_registry import ( + get_permission_registry, + register_plugin_permissions, + ) + + if plugin.manifest.permissions: + register_plugin_permissions(name, plugin.manifest.permissions) + get_permission_registry()._active_plugins.add(name) + for entity_type, model_class in plugin.get_entity_models().items(): + from app.services.entity_permission_service import register_entity_model + + register_entity_model(entity_type, model_class) + # Call on_activate hook (registers event listeners) await plugin.on_activate(db, self._container, self._event_bus) diff --git a/tests/test_phase_f_agents.py b/tests/test_phase_f_agents.py index fe1b1cc..e1a7762 100644 --- a/tests/test_phase_f_agents.py +++ b/tests/test_phase_f_agents.py @@ -530,7 +530,7 @@ class TestAgentPermissions: ) as mock_resolve: mock_resolve.return_value = _permissions(permissions=["mail:read"]) with patch( - "app.plugins.builtins.ai_assistant.contracts.get_tool_registry", + "app.ai.tool_registry.get_tool_registry", return_value=tool_registry, ): ctx = await resolve_agent_permissions( @@ -575,7 +575,7 @@ class TestAgentPermissions: ) as mock_resolve: mock_resolve.return_value = _permissions(permissions=["mail:read"]) with patch( - "app.plugins.builtins.ai_assistant.contracts.get_tool_registry", + "app.ai.tool_registry.get_tool_registry", return_value=tool_registry, ): ctx = await resolve_agent_permissions( @@ -1015,7 +1015,7 @@ class TestContextBuilder: ]) with patch( - "app.plugins.builtins.ai_assistant.contracts.get_tool_registry", + "app.ai.tool_registry.get_tool_registry", return_value=tool_registry, ): messages = await build_agent_context(