fix(e7): CI-Gate-Vorbereitung — ruff über app/ von 105 auf 0 Findings bereinigt; 8 echte F821-NameError-Produktionsbugs behoben (external_api stream_chat-Call-Signatur an stream_chat_comm angepasst, agent_runner uuid vor lokalem Import, automation/plugin UserTenant-Import, tasks delete-audit user_id, workflows/engine timedelta, unified_search/contracts Any); py311-kompatibles StepHandler-Alias statt type-Statement; E402/F841 bereinigt; Verifikation 85/89 grün (4 Failures = bekannter Vorbestand BUG-099)
Check Cross-Plugin Imports / check (push) Has been cancelled

This commit is contained in:
Agent Zero
2026-08-24 13:36:17 +02:00
parent 3934aea6ef
commit 197b0d3bab
54 changed files with 151 additions and 110 deletions
+3 -1
View File
@@ -85,7 +85,9 @@
| D5-b | BUG-077 trace_plugins 27 issues: Scanner erwartete manifest.py, Projekt-Konvention ist Inline-Manifest in plugin.py; migrations/tests fälschlich als Plugins; menu_items-Findings konzeptionell falsch (dynamische Konsumtion) | ✅ Scanner gefixt: 27→0 (-100%) | — | | D5-b | BUG-077 trace_plugins 27 issues: Scanner erwartete manifest.py, Projekt-Konvention ist Inline-Manifest in plugin.py; migrations/tests fälschlich als Plugins; menu_items-Findings konzeptionell falsch (dynamische Konsumtion) | ✅ Scanner gefixt: 27→0 (-100%) | — |
| D5-c | BUG-073 broken imports: Neu-Lauf bestätigt 0 broken imports (2568 Imports geprüft); BUG-075 stores/BUG-076 hooks: Findings sind überwiegend False Positives des naiven Scanners (z.B. 'const'/'null' als Store-Member) | ✅ Dokumentiert; Scanner-Qualität als bekanntes Limit vermerkt | 5cc5a3f | | D5-c | BUG-073 broken imports: Neu-Lauf bestätigt 0 broken imports (2568 Imports geprüft); BUG-075 stores/BUG-076 hooks: Findings sind überwiegend False Positives des naiven Scanners (z.B. 'const'/'null' als Store-Member) | ✅ Dokumentiert; Scanner-Qualität als bekanntes Limit vermerkt | 5cc5a3f |
| D6-a | ARCH-059 ai_copilot Legacy-Migration: Beweise — Backend-only (0 Frontend-Referenzen), Test geskippt, keine Router-Inklusion → Migration wäre Verschwendung | ✅ Deprecated markiert (Service+Routes Docstrings mit Abschaltplan), DeprecationWarning bei Import; Entfernung als eigene Migration nach Traffic-Bestätigung; ruff clean, create_app OK | — | | D6-a | ARCH-059 ai_copilot Legacy-Migration: Beweise — Backend-only (0 Frontend-Referenzen), Test geskippt, keine Router-Inklusion → Migration wäre Verschwendung | ✅ Deprecated markiert (Service+Routes Docstrings mit Abschaltplan), DeprecationWarning bei Import; Entfernung als eigene Migration nach Traffic-Bestätigung; ruff clean, create_app OK | — |
| D6-b | ARCH-023 service_container.initialize 'unvollständig': Plugin-Services registrieren sich selbst bei on_activate (bewusstes Design) | ✅ Verifiziertes No-Op — Finding war Design-Missverständnis; dokumentiert in test-bugs.md | | | D6-b | ARCH-023 service_container.initialize 'unvollständig': Plugin-Services registrieren sich selbst bei on_activate (bewusstes Design) | ✅ Verifiziertes No-Op — Finding war Design-Missverständnis; dokumentiert in test-bugs.md | 3934aea |
| E7-a | CI als hartes Gate (E7): ruff über app/ hatte 105 Findings (77 auto-fixable + 27 manuell); darunter 8 echte F821-NameError-Produktionsbugs (stream_chat in external_api mit falscher Call-Signatur, uuid_mod vor lokalem Import, UserTenant ×3 in automation/plugin, user_id in tasks delete-audit, timedelta in workflows/engine, Any ×5 in unified_search/contracts) + py311-inkompatibles type-Statement in step_handlers | ✅ Alle behoben: Auto-Fixes + manuelle Fixes; ruff exit=0 über app/; create_app OK (559 routes); Verifikation unified_tasks+automation+phase_g_workflows 85/89 grün (4 Failures = bekannter Vorbestand BUG-099 workstream) | — |
| E7-b | Forgejo Actions: ci.yml existiert (.forgejo/workflows/ci.yml, trigger push/PR main), aber 0 Läufe bisher (total_count=0) — Runner-Konfiguration auf Server-Seite zu prüfen; Branch-Protection 'Merge nur bei grün' ist Forgejo-Server-Einstellung | ⏳ Dokumentiert für Server-Admin: Actions-Runner aktivieren + Branch-Protection setzen; Pipeline-Inhalt ist vollständig (15 Checks) | — |
**Block D ABGESCHLOSSEN** (D1D6) — D1: alle 9 Ziel-Suites grün; D2: DateTime/SQLITE-001; D3: ARCH-051/055/056/057 + systemischer Permission-Resolver-Bug + conftest-pgvector; D4: Security-Triage (ARCH-027 verifiziert, BUG-019 = 0 echte Secrets, BUG-020 kein fixbares Finding); D5: Scanner-Triage (api_contracts -75%, plugins -100%, 371 Fehlalarme eliminiert); D6: ai_copilot deprecated + ARCH-023 No-Op. Offene Follow-ups dokumentiert (~12 echte API-Bugs aus D5, IMAP-Mocking für Mail-Tests). Nächster Block: E (Production-Härtung). **Block D ABGESCHLOSSEN** (D1D6) — D1: alle 9 Ziel-Suites grün; D2: DateTime/SQLITE-001; D3: ARCH-051/055/056/057 + systemischer Permission-Resolver-Bug + conftest-pgvector; D4: Security-Triage (ARCH-027 verifiziert, BUG-019 = 0 echte Secrets, BUG-020 kein fixbares Finding); D5: Scanner-Triage (api_contracts -75%, plugins -100%, 371 Fehlalarme eliminiert); D6: ai_copilot deprecated + ARCH-023 No-Op. Offene Follow-ups dokumentiert (~12 echte API-Bugs aus D5, IMAP-Mocking für Mail-Tests). Nächster Block: E (Production-Härtung).
+1 -1
View File
@@ -17,7 +17,7 @@ from __future__ import annotations
import logging import logging
import uuid import uuid
from dataclasses import dataclass, field from dataclasses import dataclass
from typing import Any from typing import Any
from sqlalchemy import select from sqlalchemy import select
-2
View File
@@ -25,8 +25,6 @@ import logging
import uuid import uuid
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from app.core.sensitive_data import sanitize_dict
if TYPE_CHECKING: if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
-2
View File
@@ -20,8 +20,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.ai_use_case import AIUseCaseMetadata from app.ai.ai_use_case import AIUseCaseMetadata
from app.core.sensitive_data import ( from app.core.sensitive_data import (
SENSITIVE_FIELDS, SENSITIVE_FIELDS,
filter_for_llm_context,
get_data_class_for_field,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
-1
View File
@@ -6,7 +6,6 @@ import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any
LOW_CONFIDENCE_THRESHOLD = 0.6 LOW_CONFIDENCE_THRESHOLD = 0.6
-1
View File
@@ -7,7 +7,6 @@ from typing import Any
from app.ai.knowledge_sources import get_source_config from app.ai.knowledge_sources import get_source_config
EXTRACTION_TRIGGERS = { EXTRACTION_TRIGGERS = {
"mail.received", "mail.received",
"dms.file_uploaded", "dms.file_uploaded",
+1 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass
from typing import Any from typing import Any
+6 -2
View File
@@ -345,9 +345,11 @@ async def cleanup_audit_log_job(ctx: dict[str, Any]) -> None:
Runs daily to prevent the audit_log table from growing indefinitely. Runs daily to prevent the audit_log table from growing indefinitely.
Iterates per-tenant for RLS compliance. Iterates per-tenant for RLS compliance.
""" """
from sqlalchemy import text as sa_text, delete as sa_delete
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from sqlalchemy import delete as sa_delete
from sqlalchemy import text as sa_text
from app.core.db import get_worker_session_factory from app.core.db import get_worker_session_factory
from app.models.audit import AuditLog from app.models.audit import AuditLog
@@ -388,9 +390,11 @@ async def cleanup_trash_job(ctx: dict[str, Any]) -> None:
Runs daily to clean up the trash. Iterates per-tenant for RLS compliance. Runs daily to clean up the trash. Iterates per-tenant for RLS compliance.
Default retention: 90 days in trash before permanent deletion. Default retention: 90 days in trash before permanent deletion.
""" """
from sqlalchemy import text as sa_text, delete as sa_delete
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from sqlalchemy import delete as sa_delete
from sqlalchemy import text as sa_text
from app.core.db import get_worker_session_factory from app.core.db import get_worker_session_factory
from app.models.contact import Contact from app.models.contact import Contact
from app.models.entity_attachment import EntityAttachment from app.models.entity_attachment import EntityAttachment
+2 -2
View File
@@ -36,13 +36,14 @@ from app.routes import ( # noqa: E402
attachments, attachments,
audit, audit,
auth, auth,
compliance,
backups, backups,
bank_accounts, bank_accounts,
compliance,
currencies, currencies,
custom_field_definitions, custom_field_definitions,
custom_fields, custom_fields,
dashboard, dashboard,
delegations,
entity_history, entity_history,
entity_permissions, entity_permissions,
errors, errors,
@@ -56,7 +57,6 @@ from app.routes import ( # noqa: E402
owner_transfer, owner_transfer,
permission_templates, permission_templates,
plugins, plugins,
delegations,
policies, policies,
roles, roles,
saved_filters, saved_filters,
+1 -1
View File
@@ -6,8 +6,8 @@ from app.models.audit import AuditLog
from app.models.auth import ApiToken, PasswordResetToken from app.models.auth import ApiToken, PasswordResetToken
from app.models.backup import Backup from app.models.backup import Backup
from app.models.bank_account import BankAccount from app.models.bank_account import BankAccount
from app.models.consumer_inbox import ConsumerInbox
from app.models.compliance import ComplianceIncident from app.models.compliance import ComplianceIncident
from app.models.consumer_inbox import ConsumerInbox
from app.models.contact import Contact, ContactPerson from app.models.contact import Contact, ContactPerson
from app.models.contact_folder import ContactFolder from app.models.contact_folder import ContactFolder
from app.models.contact_merge import ContactMergeHistory from app.models.contact_merge import ContactMergeHistory
+1 -2
View File
@@ -11,13 +11,12 @@ from datetime import datetime
from typing import Any from typing import Any
from sqlalchemy import DateTime, ForeignKey, String, func from sqlalchemy import DateTime, ForeignKey, String, func
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
# Re-export EntityHistory as DeletionLog for backward compatibility. # Re-export EntityHistory as DeletionLog for backward compatibility.
# Tests import DeletionLog from app.models.audit and use entity_snapshot attribute. # Tests import DeletionLog from app.models.audit and use entity_snapshot attribute.
-1
View File
@@ -12,7 +12,6 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, SoftDeleteMixin, TimestampMixin from app.core.db import Base, SoftDeleteMixin, TimestampMixin
from app.models.owned_mixin import OwnedMixin
class User(Base, TimestampMixin, SoftDeleteMixin): class User(Base, TimestampMixin, SoftDeleteMixin):
@@ -12,7 +12,6 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from pgvector.sqlalchemy import Vector
class AgentMemory(Base, TenantMixin, OwnedMixin): class AgentMemory(Base, TenantMixin, OwnedMixin):
@@ -22,6 +22,7 @@ from app.plugins.builtins.ai_assistant.schemas import (
ExternalAgentRequest, ExternalAgentRequest,
ExternalAgentResponse, ExternalAgentResponse,
) )
from app.plugins.builtins.ai_assistant.services import stream_chat_comm as stream_chat
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -120,13 +121,18 @@ async def run_agent_external(
} }
# Run the agent via streaming chat (non-streaming mode) # Run the agent via streaming chat (non-streaming mode)
from app.plugins.builtins.ai_assistant.services import stream_chat_comm
full_response = "" full_response = ""
async with get_db() as stream_db: async with get_db() as stream_db:
await set_tenant_context(stream_db, tenant_id) await set_tenant_context(stream_db, tenant_id)
async for chunk in stream_chat( async for chunk in stream_chat(
stream_db, session, agent, data.message, user_context, tenant_id stream_db,
session.id,
agent,
data.message,
user_context,
tenant_id,
uuid.UUID(current_user["user_id"]),
): ):
if chunk.startswith("data: ") and chunk != "data: [DONE]\n\n": if chunk.startswith("data: ") and chunk != "data: [DONE]\n\n":
try: try:
@@ -69,10 +69,12 @@ async def push_suggestion(user_id: str, suggestion: dict[str, Any]) -> None:
# Post suggestion to Communication (I-WORK-PROACTIVE) # Post suggestion to Communication (I-WORK-PROACTIVE)
try: try:
import uuid as uuid_mod import uuid as uuid_mod
from sqlalchemy import select as sa_select
from app.core.db import get_worker_session_factory
from app.plugins.builtins.contracts import get_contract_registry from app.plugins.builtins.contracts import get_contract_registry
from app.plugins.builtins.kommunikation.models import CommConversation from app.plugins.builtins.kommunikation.models import CommConversation
from sqlalchemy import select as sa_select
from app.core.db import get_worker_session_factory
komm = get_contract_registry().get("kommunikation") komm = get_contract_registry().get("kommunikation")
if komm: if komm:
factory = get_worker_session_factory() factory = get_worker_session_factory()
@@ -55,7 +55,8 @@ async def send_agent_message(
# 2. Create a kommunikation message in a dedicated agent room # 2. Create a kommunikation message in a dedicated agent room
try: try:
from app.plugins.builtins.kommunikation.contracts import CommConversation as Room, CommMessage as Message from app.plugins.builtins.kommunikation.contracts import CommConversation as Room
from app.plugins.builtins.kommunikation.contracts import CommMessage as Message
# Find or create the agent-to-agent room # Find or create the agent-to-agent room
room_name = f"agent:{from_agent_id}:{target_agent.id}" room_name = f"agent:{from_agent_id}:{target_agent.id}"
@@ -619,6 +619,7 @@ async def stream_agent_run(
in real-time as the agent processes. in real-time as the agent processes.
""" """
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from app.ai.agent_stream import stream_react_loop from app.ai.agent_stream import stream_react_loop
from app.plugins.builtins.ai_assistant.contracts import get_tool_registry from app.plugins.builtins.ai_assistant.contracts import get_tool_registry
@@ -11,6 +11,7 @@ Safety features:
from __future__ import annotations from __future__ import annotations
import logging import logging
import uuid
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any from typing import Any
@@ -37,12 +38,12 @@ async def run_agent(
3. Infinite loop: same tool 5x consecutively (handled in ReAct loop) 3. Infinite loop: same tool 5x consecutively (handled in ReAct loop)
4. Budget limit: cumulative cost_usd 4. Budget limit: cumulative cost_usd
""" """
from app.plugins.builtins.ai_assistant.contracts import get_tool_registry
from app.plugins.builtins.automation.models import ( from app.plugins.builtins.automation.models import (
AgentDefinition, AgentDefinition,
AgentRun, AgentRun,
AgentRunStep, AgentRunStep,
) )
from app.plugins.builtins.ai_assistant.contracts import get_tool_registry
factory = get_session_factory() factory = get_session_factory()
@@ -167,7 +168,7 @@ async def run_agent(
perm_ctx = await resolve_agent_permissions( perm_ctx = await resolve_agent_permissions(
db=db, db=db,
tenant_id=agent.tenant_id, tenant_id=agent.tenant_id,
user_id=agent.created_by or uuid_mod.uuid4(), user_id=agent.created_by or uuid.uuid4(),
agent_definition=agent, agent_definition=agent,
) )
+15 -9
View File
@@ -248,17 +248,23 @@ class AutomationPlugin(BasePlugin):
logger.exception("Failed to register own cron jobs") logger.exception("Failed to register own cron jobs")
# Register pre-built agents in DB (if not already present) # Register pre-built agents in DB (if not already present)
try: try:
from app.plugins.builtins.automation.models import AgentDefinition
from app.plugins.builtins.automation.prebuilt.email_triage_agent import create_email_triage_agent
from app.plugins.builtins.automation.prebuilt.contact_enrichment_agent import create_contact_enrichment_agent
from app.plugins.builtins.automation.prebuilt.follow_up_agent import create_follow_up_agent
from app.plugins.builtins.automation.prebuilt.report_agent import create_report_agent
from sqlalchemy import select as sa_select from sqlalchemy import select as sa_select
# Get system tenant + admin user for seeding (ARCH-043: # Get system tenant + admin user for seeding (ARCH-043:
# deterministic slug lookup instead of arbitrary first row) # deterministic slug lookup instead of arbitrary first row)
from app.core.db import get_system_tenant from app.core.db import get_system_tenant
from app.models.user import User from app.models.user import User, UserTenant
from app.plugins.builtins.automation.models import AgentDefinition
from app.plugins.builtins.automation.prebuilt.contact_enrichment_agent import (
create_contact_enrichment_agent,
)
from app.plugins.builtins.automation.prebuilt.email_triage_agent import (
create_email_triage_agent,
)
from app.plugins.builtins.automation.prebuilt.follow_up_agent import (
create_follow_up_agent,
)
from app.plugins.builtins.automation.prebuilt.report_agent import create_report_agent
tenant = await get_system_tenant(db) tenant = await get_system_tenant(db)
if tenant: if tenant:
@@ -298,15 +304,14 @@ class AutomationPlugin(BasePlugin):
def _register_workflow_agent_tools(self) -> None: def _register_workflow_agent_tools(self) -> None:
"""Register I-AW agent tools for starting and inspecting workflows.""" """Register I-AW agent tools for starting and inspecting workflows."""
import uuid import uuid
from typing import Any
from app.ai.tool_registry import get_tool_registry from app.ai.tool_registry import get_tool_registry
registry = get_tool_registry() registry = get_tool_registry()
async def _start_workflow_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: async def _start_workflow_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
"""Start a workflow by ID.""" """Start a workflow by ID."""
from app.services.workflow_service import create_instance
from app.core.db import get_worker_session_factory from app.core.db import get_worker_session_factory
from app.services.workflow_service import create_instance
workflow_id = arguments.get("workflow_id", "") workflow_id = arguments.get("workflow_id", "")
tenant_id = context.get("tenant_id") tenant_id = context.get("tenant_id")
user_id = context.get("user_id") user_id = context.get("user_id")
@@ -342,8 +347,9 @@ class AutomationPlugin(BasePlugin):
async def _check_workflow_status_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: async def _check_workflow_status_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
"""Check the status of a workflow instance.""" """Check the status of a workflow instance."""
from sqlalchemy import select from sqlalchemy import select
from app.models.workflow import WorkflowInstance
from app.core.db import get_worker_session_factory from app.core.db import get_worker_session_factory
from app.models.workflow import WorkflowInstance
instance_id = arguments.get("instance_id", "") instance_id = arguments.get("instance_id", "")
tenant_id = context.get("tenant_id") tenant_id = context.get("tenant_id")
if not instance_id or not tenant_id: if not instance_id or not tenant_id:
@@ -3,7 +3,9 @@
Enriches contact data by searching for related information. Enriches contact data by searching for related information.
""" """
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from app.plugins.builtins.automation.models import AgentDefinition from app.plugins.builtins.automation.models import AgentDefinition
CONTACT_ENRICHMENT_SYSTEM_PROMPT = """You are a Contact Enrichment Agent for a CRM system. CONTACT_ENRICHMENT_SYSTEM_PROMPT = """You are a Contact Enrichment Agent for a CRM system.
@@ -3,7 +3,9 @@
Sorts and prioritizes incoming emails automatically. Sorts and prioritizes incoming emails automatically.
""" """
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from app.plugins.builtins.automation.models import AgentDefinition from app.plugins.builtins.automation.models import AgentDefinition
EMAIL_TRIAGE_SYSTEM_PROMPT = """You are an E-Mail Triage Agent for a CRM system. EMAIL_TRIAGE_SYSTEM_PROMPT = """You are an E-Mail Triage Agent for a CRM system.
@@ -3,7 +3,9 @@
Reminds about and creates follow-up tasks for contacts. Reminds about and creates follow-up tasks for contacts.
""" """
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from app.plugins.builtins.automation.models import AgentDefinition from app.plugins.builtins.automation.models import AgentDefinition
FOLLOW_UP_SYSTEM_PROMPT = """You are a Follow-up Agent for a CRM system. FOLLOW_UP_SYSTEM_PROMPT = """You are a Follow-up Agent for a CRM system.
@@ -3,7 +3,9 @@
Generates reports from CRM data using search and API tools. Generates reports from CRM data using search and API tools.
""" """
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from app.plugins.builtins.automation.models import AgentDefinition from app.plugins.builtins.automation.models import AgentDefinition
REPORT_SYSTEM_PROMPT = """You are a Report Agent for a CRM system. REPORT_SYSTEM_PROMPT = """You are a Report Agent for a CRM system.
@@ -7,7 +7,12 @@ PGUUID/JSONB column types.
from __future__ import annotations from __future__ import annotations
# Register ALL plugin models so create_all can resolve cross-plugin FKs
# (e.g. entity_attachments.dms_file_id -> files) — same pattern as
# scripts/sync_plugin_schema.py.
import importlib
import os import os
import pkgutil
import uuid import uuid
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
@@ -17,17 +22,10 @@ import pytest_asyncio
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.core.db import Base
import app.models # noqa: F401 — registers core models import app.models # noqa: F401 — registers core models
import app.models.outbox # noqa: F401 — event_outbox is NOT re-exported by app.models import app.models.outbox # noqa: F401 — event_outbox is NOT re-exported by app.models
# Register ALL plugin models so create_all can resolve cross-plugin FKs
# (e.g. entity_attachments.dms_file_id -> files) — same pattern as
# scripts/sync_plugin_schema.py.
import importlib
import pkgutil
import app.plugins.builtins as _builtins_pkg import app.plugins.builtins as _builtins_pkg
from app.core.db import Base
for _importer, _modname, _ispkg in pkgutil.iter_modules(_builtins_pkg.__path__): for _importer, _modname, _ispkg in pkgutil.iter_modules(_builtins_pkg.__path__):
if not _ispkg: if not _ispkg:
@@ -39,11 +37,11 @@ for _importer, _modname, _ispkg in pkgutil.iter_modules(_builtins_pkg.__path__):
except Exception: # pragma: no cover - defensive except Exception: # pragma: no cover - defensive
pass pass
from app.plugins.builtins.automation.models import ( from app.plugins.builtins.automation.models import ( # noqa: E402 — after dynamic plugin-model discovery
AgentRun, AgentRun,
AutomationRun, AutomationRun,
) )
from app.plugins.builtins.automation.services import ( from app.plugins.builtins.automation.services import ( # noqa: E402 — after dynamic plugin-model discovery
AgentService, AgentService,
AutomationService, AutomationService,
CronJobService, CronJobService,
+2 -3
View File
@@ -6,6 +6,7 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from pgvector.sqlalchemy import Vector
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
DateTime, DateTime,
@@ -13,14 +14,12 @@ from sqlalchemy import (
Index, Index,
String, String,
) )
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
from pgvector.sqlalchemy import Vector
class Calendar(Base, TenantMixin, OwnedMixin): class Calendar(Base, TenantMixin, OwnedMixin):
+1 -2
View File
@@ -21,6 +21,7 @@ from fastapi.responses import StreamingResponse
from sqlalchemy import select, update from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.audit import log_audit
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user, require_admin, require_permission from app.deps import get_current_user, require_admin, require_permission
from app.plugins.builtins.calendar.ics_utils import ( from app.plugins.builtins.calendar.ics_utils import (
@@ -1023,5 +1024,3 @@ async def book_resource(
"start_at": booking.start_at.isoformat(), "start_at": booking.start_at.isoformat(),
"end_at": booking.end_at.isoformat(), "end_at": booking.end_at.isoformat(),
} }
from app.core.audit import log_audit
+4 -4
View File
@@ -4,16 +4,16 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, UniqueConstraint, Text from pgvector.sqlalchemy import Vector
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import TSVECTOR
from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from pgvector.sqlalchemy import Vector
from sqlalchemy.dialects.postgresql import TSVECTOR
from typing import Any
class Folder(Base, TenantMixin, OwnedMixin): class Folder(Base, TenantMixin, OwnedMixin):
+2 -1
View File
@@ -25,7 +25,8 @@ async def cleanup_knowledge_job(ctx: dict[str, Any]) -> None:
Runs daily. Keeps approved extractions indefinitely. Runs daily. Keeps approved extractions indefinitely.
Iterates per-tenant for RLS compliance. Iterates per-tenant for RLS compliance.
""" """
from sqlalchemy import text as sa_text, delete as sa_delete from sqlalchemy import delete as sa_delete
from sqlalchemy import text as sa_text
from app.core.db import get_worker_session_factory from app.core.db import get_worker_session_factory
+7 -2
View File
@@ -1,12 +1,17 @@
"""Knowledge extraction models — tracks LLM extractions and review queue.""" """Knowledge extraction models — tracks LLM extractions and review queue."""
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from datetime import datetime from datetime import datetime
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID from sqlalchemy import DateTime, Float, ForeignKey, Index, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
class KnowledgeExtraction(Base, TenantMixin): class KnowledgeExtraction(Base, TenantMixin):
"""Tracks a single knowledge extraction run from a source (wiki, dms, mail, comm).""" """Tracks a single knowledge extraction run from a source (wiki, dms, mail, comm)."""
__tablename__ = "knowledge_extractions" __tablename__ = "knowledge_extractions"
+4 -2
View File
@@ -1,8 +1,10 @@
"""Knowledge plugin — LLM-based entity/relationship extraction, ask-knowledge, review queue.""" """Knowledge plugin — LLM-based entity/relationship extraction, ask-knowledge, review queue."""
from __future__ import annotations from __future__ import annotations
import logging import logging
import uuid import uuid
from typing import Any from typing import Any
from app.plugins.base import BasePlugin from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest, PluginRouteDef from app.plugins.manifest import PluginManifest, PluginRouteDef
@@ -78,8 +80,8 @@ class KnowledgePlugin(BasePlugin):
async def _ask_knowledge_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: async def _ask_knowledge_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
"""Ask a knowledge question.""" """Ask a knowledge question."""
from app.plugins.builtins.knowledge.services import ask_knowledge
from app.core.db import get_worker_session_factory from app.core.db import get_worker_session_factory
from app.plugins.builtins.knowledge.services import ask_knowledge
question = arguments.get("question", "") question = arguments.get("question", "")
tenant_id = context.get("tenant_id") tenant_id = context.get("tenant_id")
if not question or not tenant_id: if not question or not tenant_id:
@@ -107,8 +109,8 @@ class KnowledgePlugin(BasePlugin):
async def _search_knowledge_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: async def _search_knowledge_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
"""Search wiki articles via unified search.""" """Search wiki articles via unified search."""
from app.plugins.builtins.unified_search.provider_registry import get_search_registry
from app.core.db import get_worker_session_factory from app.core.db import get_worker_session_factory
from app.plugins.builtins.unified_search.provider_registry import get_search_registry
query = arguments.get("query", "") query = arguments.get("query", "")
tenant_id = context.get("tenant_id") tenant_id = context.get("tenant_id")
if not query or not tenant_id: if not query or not tenant_id:
+4 -1
View File
@@ -1,10 +1,13 @@
"""Knowledge extraction services — LLM-based entity/relationship extraction.""" """Knowledge extraction services — LLM-based entity/relationship extraction."""
from __future__ import annotations from __future__ import annotations
import logging import logging
import uuid import uuid
from typing import Any from typing import Any
from sqlalchemy import select, update
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.llm_client import llm_complete from app.ai.llm_client import llm_complete
from app.plugins.builtins.knowledge.models import KnowledgeExtraction from app.plugins.builtins.knowledge.models import KnowledgeExtraction
+3 -3
View File
@@ -4,7 +4,9 @@ from __future__ import annotations
import uuid import uuid
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any
from pgvector.sqlalchemy import Vector
from sqlalchemy import ( from sqlalchemy import (
JSON, JSON,
Boolean, Boolean,
@@ -16,14 +18,12 @@ from sqlalchemy import (
Text, Text,
UniqueConstraint, UniqueConstraint,
) )
from sqlalchemy.dialects.postgresql import TSVECTOR
from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
from pgvector.sqlalchemy import Vector
from typing import Any
# --- Mail Accounts (F-MAIL-14, F-MAIL-18) --- # --- Mail Accounts (F-MAIL-14, F-MAIL-18) ---
-1
View File
@@ -1862,4 +1862,3 @@ async def get_mail(
) )
return mail_to_response(mail, attachments=list(attachments), labels=list(label_assignments)) return mail_to_response(mail, attachments=list(attachments), labels=list(label_assignments))
from app.core.audit import log_audit
@@ -18,7 +18,8 @@ from sqlalchemy import (
Text, Text,
func, func,
) )
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
@@ -21,11 +21,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.llm_client import llm_complete from app.ai.llm_client import llm_complete
from app.plugins.builtins.self_improvement.models import ( from app.plugins.builtins.self_improvement.models import (
ImpactMeasurement,
ImprovementPattern, ImprovementPattern,
ImprovementProposal, ImprovementProposal,
ImprovementSignal, ImprovementSignal,
ImpactMeasurement,
PROPOSAL_STATUSES,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+4 -4
View File
@@ -3,16 +3,16 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from typing import Any
from pgvector.sqlalchemy import Vector
from sqlalchemy import ForeignKey, Index, String, UniqueConstraint from sqlalchemy import ForeignKey, Index, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID as PGUUID, JSONB from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
from pgvector.sqlalchemy import Vector
from typing import Any
class Tag(Base, TenantMixin, OwnedMixin): class Tag(Base, TenantMixin, OwnedMixin):
+1 -2
View File
@@ -8,6 +8,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Response, status
from sqlalchemy import delete, func, select from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.audit import log_audit
from app.core.db import get_db from app.core.db import get_db
from app.core.visibility import apply_visibility_filter from app.core.visibility import apply_visibility_filter
from app.deps import get_current_user, require_permission from app.deps import get_current_user, require_permission
@@ -389,5 +390,3 @@ async def list_tag_entities(
} }
for a in assignments for a in assignments
] ]
from app.core.audit import log_audit
+2 -2
View File
@@ -7,6 +7,7 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.audit import log_audit
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user, require_permission from app.deps import get_current_user, require_permission
from app.plugins.builtins.tasks import services from app.plugins.builtins.tasks import services
@@ -126,6 +127,7 @@ async def delete_task(
): ):
"""Delete a task (soft-delete).""" """Delete a task (soft-delete)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
tid = _parse_uuid(task_id, "task_id") tid = _parse_uuid(task_id, "task_id")
deleted = await services.delete_task(db, tenant_id, tid) deleted = await services.delete_task(db, tenant_id, tid)
if not deleted: if not deleted:
@@ -261,5 +263,3 @@ async def decompose_goal(
if result is None: if result is None:
raise HTTPException(404, detail={"detail": "Goal not found", "code": "not_found"}) raise HTTPException(404, detail={"detail": "Goal not found", "code": "not_found"})
return result return result
from app.core.audit import log_audit
-1
View File
@@ -7,7 +7,6 @@ from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
TASK_STATUSES = "^(open|in_progress|review|blocked|done|cancelled)$" TASK_STATUSES = "^(open|in_progress|review|blocked|done|cancelled)$"
TASK_TYPES = "^(todo|approval|follow_up|review|goal|milestone|agent_subtask)$" TASK_TYPES = "^(todo|approval|follow_up|review|goal|milestone|agent_subtask)$"
ASSIGNEE_TYPES = "^(user|agent|group)$" ASSIGNEE_TYPES = "^(user|agent|group)$"
+4 -6
View File
@@ -16,12 +16,11 @@ def _to_uuid(val: str | UUID | None) -> UUID | None:
return val return val
return uuid.UUID(str(val)) return uuid.UUID(str(val))
from sqlalchemy import func, select from sqlalchemy import func, select # noqa: E402 — after helper defs by design
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession # noqa: E402
from app.core.visibility import apply_visibility_filter
from app.plugins.builtins.tasks.models import Task
from app.core.visibility import apply_visibility_filter # noqa: E402
from app.plugins.builtins.tasks.models import Task # noqa: E402
# Lifecycle statuses in display order (Kanban columns) # Lifecycle statuses in display order (Kanban columns)
STATUS_ORDER = ["open", "in_progress", "review", "blocked", "done", "cancelled"] STATUS_ORDER = ["open", "in_progress", "review", "blocked", "done", "cancelled"]
@@ -307,7 +306,6 @@ async def create_task(
entity_id = contact_id entity_id = contact_id
# Don't store contact_id in FK column if it's just a polymorphic entity link # Don't store contact_id in FK column if it's just a polymorphic entity link
# — contact_id FK requires a real Contact row. Use entity_id instead. # — contact_id FK requires a real Contact row. Use entity_id instead.
contact_id_for_fk = _to_uuid(contact_id) if contact_id else None
# Resolve polymorphic creator. # Resolve polymorphic creator.
creator_type = data.get("creator_type", "user") creator_type = data.get("creator_type", "user")
@@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from app.plugins.builtins.contracts import get_contract_registry from app.plugins.builtins.contracts import get_contract_registry
from app.plugins.builtins.unified_search.base_provider import BaseSearchProvider from app.plugins.builtins.unified_search.base_provider import BaseSearchProvider
from app.plugins.builtins.unified_search.embedding import generate_embedding from app.plugins.builtins.unified_search.embedding import generate_embedding
@@ -50,8 +50,7 @@ class SearchIndexLog(Base, TenantMixin, OwnedMixin):
status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending") status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
from pgvector.sqlalchemy import Vector # noqa: E402 from pgvector.sqlalchemy import Vector # noqa: E402 — optional dependency import after model defs
from app.models.owned_mixin import OwnedMixin
class DocumentChunk(Base, TenantMixin, OwnedMixin): class DocumentChunk(Base, TenantMixin, OwnedMixin):
+5 -1
View File
@@ -1,6 +1,8 @@
"""Wiki plugin - knowledge articles, categories, versioning.""" """Wiki plugin - knowledge articles, categories, versioning."""
from __future__ import annotations from __future__ import annotations
import logging import logging
from app.plugins.base import BasePlugin from app.plugins.base import BasePlugin
from app.plugins.manifest import FrontendMenuItem, FrontendPageRoute, PluginManifest, PluginRouteDef from app.plugins.manifest import FrontendMenuItem, FrontendPageRoute, PluginManifest, PluginRouteDef
@@ -28,7 +30,9 @@ class WikiPlugin(BasePlugin):
from app.plugins.builtins.contracts import get_contract from app.plugins.builtins.contracts import get_contract
search_contract = get_contract("unified_search") search_contract = get_contract("unified_search")
if search_contract and hasattr(search_contract, "get_search_registry"): if search_contract and hasattr(search_contract, "get_search_registry"):
from app.plugins.builtins.unified_search.providers.wiki_provider import WikiSearchProvider from app.plugins.builtins.unified_search.providers.wiki_provider import (
WikiSearchProvider,
)
search_contract.get_search_registry().register(WikiSearchProvider()) search_contract.get_search_registry().register(WikiSearchProvider())
logger.info("Registered WikiSearchProvider via contract") logger.info("Registered WikiSearchProvider via contract")
else: else:
+11 -4
View File
@@ -1,13 +1,20 @@
"""Wiki plugin routes — articles CRUD, categories, versioning.""" """Wiki plugin routes — articles CRUD, categories, versioning."""
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.deps import get_current_user, require_permission
from app.plugins.builtins.wiki import services
from app.plugins.builtins.wiki.schemas import ArticleCreate, ArticleUpdate, CategoryCreate, CategoryUpdate
from app.core.audit import log_audit from app.core.audit import log_audit
from app.core.db import get_db
from app.deps import require_permission
from app.plugins.builtins.wiki import services
from app.plugins.builtins.wiki.schemas import (
ArticleCreate,
ArticleUpdate,
CategoryCreate,
)
router = APIRouter(prefix="/api/v1/wiki", tags=["wiki"]) router = APIRouter(prefix="/api/v1/wiki", tags=["wiki"])
+2 -1
View File
@@ -1,8 +1,9 @@
"""Pydantic schemas for the Wiki plugin.""" """Pydantic schemas for the Wiki plugin."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
class CategoryCreate(BaseModel): class CategoryCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=200) name: str = Field(..., min_length=1, max_length=200)
slug: str = Field(..., min_length=1, max_length=200) slug: str = Field(..., min_length=1, max_length=200)
+4 -2
View File
@@ -1,11 +1,13 @@
"""Wiki plugin services — CRUD for articles, categories, versioning.""" """Wiki plugin services — CRUD for articles, categories, versioning."""
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any from typing import Any
from sqlalchemy import select, func
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.visibility import apply_visibility_filter
from app.plugins.builtins.wiki.models import WikiArticle, WikiArticleVersion, WikiCategory from app.plugins.builtins.wiki.models import WikiArticle, WikiArticleVersion, WikiCategory
+1 -1
View File
@@ -4,9 +4,9 @@ from app.routes import (
addresses, # noqa: F401 addresses, # noqa: F401
attachments, # noqa: F401 attachments, # noqa: F401
audit, # noqa: F401 audit, # noqa: F401
compliance, # noqa: F401
auth, # noqa: F401 auth, # noqa: F401
bank_accounts, # noqa: F401 bank_accounts, # noqa: F401
compliance, # noqa: F401
currencies, # noqa: F401 currencies, # noqa: F401
dashboard, # noqa: F401 dashboard, # noqa: F401
entity_history, # noqa: F401 entity_history, # noqa: F401
+1 -2
View File
@@ -7,7 +7,7 @@ from __future__ import annotations
import logging import logging
import uuid import uuid
from datetime import UTC, datetime from datetime import datetime
from typing import Any from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
@@ -16,7 +16,6 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.approval import ( from app.core.approval import (
APPROVAL_STATUSES,
ApprovalRequest, ApprovalRequest,
create_approval_request, create_approval_request,
expire_approval_request, expire_approval_request,
+1 -1
View File
@@ -10,7 +10,7 @@ from datetime import UTC, datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from sqlalchemy import func, select, delete from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db from app.core.db import get_db
+1 -1
View File
@@ -11,7 +11,7 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy import select, func from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.ai_use_case import AIUseCaseMetadata, validate_ai_use_case from app.ai.ai_use_case import AIUseCaseMetadata, validate_ai_use_case
+1 -2
View File
@@ -10,8 +10,7 @@ from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db from app.core.db import get_db
from app.deps import require_admin, require_permission from app.deps import get_current_user, require_admin, require_permission
from app.deps import get_current_user
from app.plugins.migration_runner import MigrationValidationError from app.plugins.migration_runner import MigrationValidationError
from app.services.plugin_service import get_plugin_service from app.services.plugin_service import get_plugin_service
+11 -7
View File
@@ -173,7 +173,6 @@ async def create_instance(
"""Create a new workflow instance.""" """Create a new workflow instance."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
is_admin = current_user.get("is_system_admin", False)
try: try:
result = await workflow_service.create_instance( result = await workflow_service.create_instance(
@@ -304,10 +303,11 @@ async def resume_instance(
"""Resume a waiting workflow instance (e.g. after wait timer expired).""" """Resume a waiting workflow instance (e.g. after wait timer expired)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
from app.workflows.engine import WorkflowEngine
from app.models.workflow import WorkflowInstance
from sqlalchemy import select from sqlalchemy import select
from app.models.workflow import WorkflowInstance
from app.workflows.engine import WorkflowEngine
result = await db.execute( result = await db.execute(
select(WorkflowInstance).where( select(WorkflowInstance).where(
WorkflowInstance.id == uuid.UUID(instance_id), WorkflowInstance.id == uuid.UUID(instance_id),
@@ -375,9 +375,10 @@ async def webhook_trigger(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Incoming webhook trigger — starts a workflow via secure token.""" """Incoming webhook trigger — starts a workflow via secure token."""
from app.models.webhook import Webhook
from sqlalchemy import select from sqlalchemy import select
from app.models.webhook import Webhook
result = await db.execute( result = await db.execute(
select(Webhook).where( select(Webhook).where(
Webhook.token == token, Webhook.token == token,
@@ -423,9 +424,10 @@ async def get_instance_history(
"""Get step history for a workflow instance (execution log).""" """Get step history for a workflow instance (execution log)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
from app.models.workflow import WorkflowStepHistory
from sqlalchemy import select from sqlalchemy import select
from app.models.workflow import WorkflowStepHistory
result = await db.execute( result = await db.execute(
select(WorkflowStepHistory) select(WorkflowStepHistory)
.where( .where(
@@ -472,9 +474,10 @@ async def approve_workflow_step(
body = {} body = {}
comment = body.get("comment", "") comment = body.get("comment", "")
from sqlalchemy import select
from app.core.approval import create_approval_request, resolve_approval_request from app.core.approval import create_approval_request, resolve_approval_request
from app.models.workflow import WorkflowInstance from app.models.workflow import WorkflowInstance
from sqlalchemy import select
result = await db.execute( result = await db.execute(
select(WorkflowInstance).where( select(WorkflowInstance).where(
@@ -535,9 +538,10 @@ async def reject_workflow_step(
body = {} body = {}
comment = body.get("comment", "") comment = body.get("comment", "")
from sqlalchemy import select
from app.core.approval import create_approval_request, resolve_approval_request from app.core.approval import create_approval_request, resolve_approval_request
from app.models.workflow import WorkflowInstance from app.models.workflow import WorkflowInstance
from sqlalchemy import select
result = await db.execute( result = await db.execute(
select(WorkflowInstance).where( select(WorkflowInstance).where(
+2 -3
View File
@@ -15,7 +15,7 @@ from __future__ import annotations
import logging import logging
import time import time
import uuid import uuid
from datetime import UTC, datetime from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
from sqlalchemy import select from sqlalchemy import select
@@ -313,7 +313,6 @@ class WorkflowEngine:
return _instance_to_dict(instance) return _instance_to_dict(instance)
else: else:
# Retry: stay on same step, set resume_at with backoff # Retry: stay on same step, set resume_at with backoff
import asyncio
backoff = min(2 ** instance.retry_count, 60) backoff = min(2 ** instance.retry_count, 60)
from datetime import timedelta from datetime import timedelta
instance.resume_at = datetime.now(UTC) + timedelta(seconds=backoff) instance.resume_at = datetime.now(UTC) + timedelta(seconds=backoff)
@@ -572,8 +571,8 @@ class WorkflowEngine:
Prevents two workers from processing the same instance simultaneously. Prevents two workers from processing the same instance simultaneously.
""" """
from app.core.redis import get_redis from app.core.redis import get_redis
import redis.asyncio as aioredis
try: try:
r = await get_redis() r = await get_redis()
+3 -2
View File
@@ -14,7 +14,6 @@ import uuid
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.models.workflow import WorkflowInstance from app.models.workflow import WorkflowInstance
@@ -45,7 +44,8 @@ class StepResult:
self.abort = abort self.abort = abort
type StepHandler = Any # Callable[..., Awaitable[StepResult]] # Callable[..., Awaitable[StepResult]] — classic alias for py3.11 compat
StepHandler = Any
# ─── Registry ──────────────────────────────────────────────────────────────── # ─── Registry ────────────────────────────────────────────────────────────────
@@ -534,6 +534,7 @@ async def _handle_webhook(
return StepResult(error=f"URL blocked by SSRF protection: {url}", abort=True) return StepResult(error=f"URL blocked by SSRF protection: {url}", abort=True)
import json import json
import httpx import httpx
try: try: