feat(admin): consolidar governanca segura de tools na fase 5

feat/self-evolving-tools-foundation
parent b3662906bc
commit 3dcf80eaaa

@ -15,7 +15,9 @@ from admin_app.repositories import (
AuditLogRepository,
StaffAccountRepository,
StaffSessionRepository,
ToolArtifactRepository,
ToolDraftRepository,
ToolMetadataRepository,
ToolVersionRepository,
)
from admin_app.services import (
@ -64,6 +66,14 @@ def get_tool_version_repository(db: Session = Depends(get_admin_db)) -> ToolVers
return ToolVersionRepository(db)
def get_tool_metadata_repository(db: Session = Depends(get_admin_db)) -> ToolMetadataRepository:
return ToolMetadataRepository(db)
def get_tool_artifact_repository(db: Session = Depends(get_admin_db)) -> ToolArtifactRepository:
return ToolArtifactRepository(db)
def get_audit_service(
repository: AuditLogRepository = Depends(get_audit_log_repository),
) -> AuditService:
@ -100,11 +110,15 @@ def get_tool_management_service(
settings: AdminSettings = Depends(get_settings),
draft_repository: ToolDraftRepository = Depends(get_tool_draft_repository),
version_repository: ToolVersionRepository = Depends(get_tool_version_repository),
metadata_repository: ToolMetadataRepository = Depends(get_tool_metadata_repository),
artifact_repository: ToolArtifactRepository = Depends(get_tool_artifact_repository),
) -> ToolManagementService:
return ToolManagementService(
settings=settings,
draft_repository=draft_repository,
version_repository=version_repository,
metadata_repository=metadata_repository,
artifact_repository=artifact_repository,
)

@ -772,6 +772,13 @@ class AdminToolReviewQueueResponse(BaseModel):
supported_statuses: list[ToolLifecycleStatus]
class AdminToolPublicationParameterResponse(BaseModel):
name: str
parameter_type: ToolParameterType
description: str
required: bool
class AdminToolPublicationSummaryResponse(BaseModel):
publication_id: str
tool_name: str
@ -781,6 +788,8 @@ class AdminToolPublicationSummaryResponse(BaseModel):
version: int
status: ToolLifecycleStatus
parameter_count: int
parameters: list[AdminToolPublicationParameterResponse] = Field(default_factory=list)
author_name: str | None = None
implementation_module: str
implementation_callable: str
published_by: str | None = None

@ -6,9 +6,9 @@ Cria tabelas do dominio administrativo de forma explicita, fora do startup do ap
from sqlalchemy import inspect, text
from admin_app.db.database import AdminBase, admin_engine
from admin_app.db.models import AuditLog, StaffAccount, StaffSession, ToolDraft, ToolVersion
from admin_app.db.models import AuditLog, StaffAccount, StaffSession, ToolArtifact, ToolDraft, ToolMetadata, ToolVersion
_REGISTERED_MODELS = (AuditLog, StaffAccount, StaffSession, ToolDraft, ToolVersion)
_REGISTERED_MODELS = (AuditLog, StaffAccount, StaffSession, ToolArtifact, ToolDraft, ToolMetadata, ToolVersion)
def _ensure_admin_schema_evolution() -> None:

@ -2,7 +2,9 @@ from admin_app.db.models.audit_log import AuditLog
from admin_app.db.models.base import AdminTimestampedModel
from admin_app.db.models.staff_account import StaffAccount
from admin_app.db.models.staff_session import StaffSession
from admin_app.db.models.tool_artifact import ToolArtifact
from admin_app.db.models.tool_draft import ToolDraft
from admin_app.db.models.tool_metadata import ToolMetadata
from admin_app.db.models.tool_version import ToolVersion
__all__ = [
@ -10,6 +12,8 @@ __all__ = [
"AuditLog",
"StaffAccount",
"StaffSession",
"ToolArtifact",
"ToolDraft",
"ToolMetadata",
"ToolVersion",
]

@ -0,0 +1,113 @@
from __future__ import annotations
from enum import Enum
from sqlalchemy import ForeignKey, Integer, JSON, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.types import TypeDecorator
from admin_app.db.models.base import AdminTimestampedModel
class ToolArtifactStage(str, Enum):
GENERATION = "generation"
VALIDATION = "validation"
class ToolArtifactKind(str, Enum):
GENERATION_REQUEST = "generation_request"
VALIDATION_REPORT = "validation_report"
class ToolArtifactStorageKind(str, Enum):
INLINE_JSON = "inline_json"
class ToolArtifactStatus(str, Enum):
PENDING = "pending"
SUCCEEDED = "succeeded"
FAILED = "failed"
class ToolArtifactEnumType(TypeDecorator):
impl = String(40)
cache_ok = True
def __init__(self, enum_cls: type[Enum], *, length: int = 40):
super().__init__(length=length)
self.enum_cls = enum_cls
@property
def python_type(self):
return self.enum_cls
def process_bind_param(self, value, dialect):
if value is None:
return None
if isinstance(value, self.enum_cls):
return value.value
return self.enum_cls(str(value).strip().lower()).value
def process_result_value(self, value, dialect):
if value is None:
return None
return self.enum_cls(str(value).strip().lower())
class ToolArtifact(AdminTimestampedModel):
__tablename__ = "tool_artifacts"
__table_args__ = (
UniqueConstraint(
"tool_version_id",
"artifact_kind",
name="uq_tool_artifacts_tool_version_kind",
),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
artifact_id: Mapped[str] = mapped_column(String(140), unique=True, index=True, nullable=False)
draft_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("tool_drafts.id"),
nullable=False,
index=True,
)
tool_version_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("tool_versions.id"),
nullable=False,
index=True,
)
tool_name: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
artifact_stage: Mapped[ToolArtifactStage] = mapped_column(
ToolArtifactEnumType(ToolArtifactStage),
nullable=False,
index=True,
)
artifact_kind: Mapped[ToolArtifactKind] = mapped_column(
ToolArtifactEnumType(ToolArtifactKind),
nullable=False,
index=True,
)
artifact_status: Mapped[ToolArtifactStatus] = mapped_column(
ToolArtifactEnumType(ToolArtifactStatus),
nullable=False,
default=ToolArtifactStatus.PENDING,
index=True,
)
storage_kind: Mapped[ToolArtifactStorageKind] = mapped_column(
ToolArtifactEnumType(ToolArtifactStorageKind),
nullable=False,
default=ToolArtifactStorageKind.INLINE_JSON,
)
summary: Mapped[str] = mapped_column(Text, nullable=False)
payload_json: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
checksum: Mapped[str | None] = mapped_column(String(64), nullable=True)
author_staff_account_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("staff_accounts.id"),
nullable=False,
index=True,
)
author_display_name: Mapped[str] = mapped_column(String(150), nullable=False)

@ -0,0 +1,54 @@
from __future__ import annotations
from sqlalchemy import ForeignKey, Integer, JSON, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from admin_app.db.models.base import AdminTimestampedModel
from admin_app.db.models.tool_draft import ToolLifecycleStatusType
from shared.contracts import ToolLifecycleStatus
class ToolMetadata(AdminTimestampedModel):
__tablename__ = "tool_metadata"
__table_args__ = (
UniqueConstraint(
"tool_name",
"version_number",
name="uq_tool_metadata_tool_name_version_number",
),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
metadata_id: Mapped[str] = mapped_column(String(120), unique=True, index=True, nullable=False)
draft_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("tool_drafts.id"),
nullable=False,
index=True,
)
tool_version_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("tool_versions.id"),
nullable=False,
unique=True,
index=True,
)
tool_name: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
display_name: Mapped[str] = mapped_column(String(120), nullable=False)
domain: Mapped[str] = mapped_column(String(40), index=True, nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False)
parameters_json: Mapped[list[dict]] = mapped_column(JSON, nullable=False, default=list)
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[ToolLifecycleStatus] = mapped_column(
ToolLifecycleStatusType(),
nullable=False,
default=ToolLifecycleStatus.DRAFT,
index=True,
)
author_staff_account_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("staff_accounts.id"),
nullable=False,
index=True,
)
author_display_name: Mapped[str] = mapped_column(String(150), nullable=False)

@ -14,6 +14,8 @@ ALLOWED_ADMIN_WRITE_TABLES: tuple[str, ...] = (
"staff_sessions",
"tool_drafts",
"tool_versions",
"tool_metadata",
"tool_artifacts",
)

@ -2,7 +2,9 @@ from admin_app.repositories.audit_log_repository import AuditLogRepository
from admin_app.repositories.base_repository import BaseRepository
from admin_app.repositories.staff_account_repository import StaffAccountRepository
from admin_app.repositories.staff_session_repository import StaffSessionRepository
from admin_app.repositories.tool_artifact_repository import ToolArtifactRepository
from admin_app.repositories.tool_draft_repository import ToolDraftRepository
from admin_app.repositories.tool_metadata_repository import ToolMetadataRepository
from admin_app.repositories.tool_version_repository import ToolVersionRepository
__all__ = [
@ -10,6 +12,8 @@ __all__ = [
"BaseRepository",
"StaffAccountRepository",
"StaffSessionRepository",
"ToolArtifactRepository",
"ToolDraftRepository",
"ToolMetadataRepository",
"ToolVersionRepository",
]

@ -0,0 +1,213 @@
from __future__ import annotations
import hashlib
import json
from sqlalchemy import select
from admin_app.db.models import ToolArtifact
from admin_app.db.models.tool_artifact import (
ToolArtifactKind,
ToolArtifactStage,
ToolArtifactStatus,
ToolArtifactStorageKind,
)
from admin_app.repositories.base_repository import BaseRepository
class ToolArtifactRepository(BaseRepository):
def list_artifacts(
self,
*,
tool_name: str | None = None,
tool_version_id: int | None = None,
artifact_stage: ToolArtifactStage | str | None = None,
artifact_kind: ToolArtifactKind | str | None = None,
) -> list[ToolArtifact]:
statement = select(ToolArtifact).order_by(
ToolArtifact.version_number.desc(),
ToolArtifact.updated_at.desc(),
ToolArtifact.created_at.desc(),
)
if tool_name:
statement = statement.where(ToolArtifact.tool_name == str(tool_name).strip().lower())
if tool_version_id is not None:
statement = statement.where(ToolArtifact.tool_version_id == tool_version_id)
if artifact_stage:
statement = statement.where(
ToolArtifact.artifact_stage == self._normalize_stage(artifact_stage)
)
if artifact_kind:
statement = statement.where(
ToolArtifact.artifact_kind == self._normalize_kind(artifact_kind)
)
return list(self.db.execute(statement).scalars().all())
def get_by_tool_version_and_kind(
self,
tool_version_id: int,
artifact_kind: ToolArtifactKind | str,
) -> ToolArtifact | None:
statement = select(ToolArtifact).where(
ToolArtifact.tool_version_id == tool_version_id,
ToolArtifact.artifact_kind == self._normalize_kind(artifact_kind),
)
return self.db.execute(statement).scalar_one_or_none()
def create(
self,
*,
draft_id: int,
tool_version_id: int,
tool_name: str,
version_number: int,
artifact_stage: ToolArtifactStage | str,
artifact_kind: ToolArtifactKind | str,
artifact_status: ToolArtifactStatus | str,
summary: str,
payload_json: dict,
author_staff_account_id: int,
author_display_name: str,
storage_kind: ToolArtifactStorageKind | str = ToolArtifactStorageKind.INLINE_JSON,
checksum: str | None = None,
commit: bool = True,
) -> ToolArtifact:
normalized_kind = self._normalize_kind(artifact_kind)
artifact = ToolArtifact(
artifact_id=self.build_artifact_id(tool_name, version_number, normalized_kind),
draft_id=draft_id,
tool_version_id=tool_version_id,
tool_name=str(tool_name or "").strip().lower(),
version_number=int(version_number),
artifact_stage=self._normalize_stage(artifact_stage),
artifact_kind=normalized_kind,
artifact_status=self._normalize_status(artifact_status),
storage_kind=self._normalize_storage_kind(storage_kind),
summary=str(summary or "").strip(),
payload_json=dict(payload_json or {}),
checksum=checksum or self._build_payload_checksum(payload_json),
author_staff_account_id=author_staff_account_id,
author_display_name=author_display_name,
)
self.db.add(artifact)
if commit:
self.db.commit()
self.db.refresh(artifact)
else:
self.db.flush()
return artifact
def update_artifact(
self,
artifact: ToolArtifact,
*,
artifact_status: ToolArtifactStatus | str,
summary: str,
payload_json: dict,
author_staff_account_id: int,
author_display_name: str,
storage_kind: ToolArtifactStorageKind | str = ToolArtifactStorageKind.INLINE_JSON,
checksum: str | None = None,
commit: bool = True,
) -> ToolArtifact:
artifact.artifact_status = self._normalize_status(artifact_status)
artifact.storage_kind = self._normalize_storage_kind(storage_kind)
artifact.summary = str(summary or "").strip()
artifact.payload_json = dict(payload_json or {})
artifact.checksum = checksum or self._build_payload_checksum(payload_json)
artifact.author_staff_account_id = author_staff_account_id
artifact.author_display_name = author_display_name
if commit:
self.db.commit()
self.db.refresh(artifact)
else:
self.db.flush()
return artifact
def upsert_version_artifact(
self,
*,
draft_id: int,
tool_version_id: int,
tool_name: str,
version_number: int,
artifact_stage: ToolArtifactStage | str,
artifact_kind: ToolArtifactKind | str,
artifact_status: ToolArtifactStatus | str,
summary: str,
payload_json: dict,
author_staff_account_id: int,
author_display_name: str,
storage_kind: ToolArtifactStorageKind | str = ToolArtifactStorageKind.INLINE_JSON,
checksum: str | None = None,
commit: bool = True,
) -> ToolArtifact:
normalized_kind = self._normalize_kind(artifact_kind)
existing = self.get_by_tool_version_and_kind(tool_version_id, normalized_kind)
if existing is None:
return self.create(
draft_id=draft_id,
tool_version_id=tool_version_id,
tool_name=tool_name,
version_number=version_number,
artifact_stage=artifact_stage,
artifact_kind=normalized_kind,
artifact_status=artifact_status,
summary=summary,
payload_json=payload_json,
author_staff_account_id=author_staff_account_id,
author_display_name=author_display_name,
storage_kind=storage_kind,
checksum=checksum,
commit=commit,
)
return self.update_artifact(
existing,
artifact_status=artifact_status,
summary=summary,
payload_json=payload_json,
author_staff_account_id=author_staff_account_id,
author_display_name=author_display_name,
storage_kind=storage_kind,
checksum=checksum,
commit=commit,
)
@staticmethod
def build_artifact_id(
tool_name: str,
version_number: int,
artifact_kind: ToolArtifactKind | str,
) -> str:
normalized_tool_name = str(tool_name or "").strip().lower()
normalized_kind = ToolArtifactRepository._normalize_kind(artifact_kind)
return f"tool_artifact::{normalized_tool_name}::v{int(version_number)}::{normalized_kind.value}"
@staticmethod
def _build_payload_checksum(payload_json: dict | None) -> str:
canonical_payload = json.dumps(payload_json or {}, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical_payload.encode("utf-8")).hexdigest()
@staticmethod
def _normalize_stage(value: ToolArtifactStage | str) -> ToolArtifactStage:
if isinstance(value, ToolArtifactStage):
return value
return ToolArtifactStage(str(value or "").strip().lower())
@staticmethod
def _normalize_kind(value: ToolArtifactKind | str) -> ToolArtifactKind:
if isinstance(value, ToolArtifactKind):
return value
return ToolArtifactKind(str(value or "").strip().lower())
@staticmethod
def _normalize_status(value: ToolArtifactStatus | str) -> ToolArtifactStatus:
if isinstance(value, ToolArtifactStatus):
return value
return ToolArtifactStatus(str(value or "").strip().lower())
@staticmethod
def _normalize_storage_kind(value: ToolArtifactStorageKind | str) -> ToolArtifactStorageKind:
if isinstance(value, ToolArtifactStorageKind):
return value
return ToolArtifactStorageKind(str(value or "").strip().lower())

@ -0,0 +1,144 @@
from __future__ import annotations
from sqlalchemy import select
from admin_app.db.models import ToolMetadata
from admin_app.repositories.base_repository import BaseRepository
from shared.contracts import ToolLifecycleStatus
class ToolMetadataRepository(BaseRepository):
def list_metadata(
self,
*,
tool_name: str | None = None,
statuses: tuple[ToolLifecycleStatus, ...] | None = None,
) -> list[ToolMetadata]:
statement = select(ToolMetadata).order_by(
ToolMetadata.version_number.desc(),
ToolMetadata.updated_at.desc(),
ToolMetadata.created_at.desc(),
)
if tool_name:
statement = statement.where(ToolMetadata.tool_name == str(tool_name).strip().lower())
if statuses:
statement = statement.where(ToolMetadata.status.in_(statuses))
return list(self.db.execute(statement).scalars().all())
def get_by_tool_version_id(self, tool_version_id: int) -> ToolMetadata | None:
statement = select(ToolMetadata).where(ToolMetadata.tool_version_id == tool_version_id)
return self.db.execute(statement).scalar_one_or_none()
def create(
self,
*,
draft_id: int,
tool_version_id: int,
tool_name: str,
display_name: str,
domain: str,
description: str,
parameters_json: list[dict],
version_number: int,
status: ToolLifecycleStatus,
author_staff_account_id: int,
author_display_name: str,
commit: bool = True,
) -> ToolMetadata:
metadata = ToolMetadata(
metadata_id=self.build_metadata_id(tool_name, version_number),
draft_id=draft_id,
tool_version_id=tool_version_id,
tool_name=tool_name,
display_name=display_name,
domain=domain,
description=description,
parameters_json=parameters_json,
version_number=version_number,
status=status,
author_staff_account_id=author_staff_account_id,
author_display_name=author_display_name,
)
self.db.add(metadata)
if commit:
self.db.commit()
self.db.refresh(metadata)
else:
self.db.flush()
return metadata
def update_metadata(
self,
metadata: ToolMetadata,
*,
display_name: str,
domain: str,
description: str,
parameters_json: list[dict],
status: ToolLifecycleStatus,
author_staff_account_id: int,
author_display_name: str,
commit: bool = True,
) -> ToolMetadata:
metadata.display_name = display_name
metadata.domain = domain
metadata.description = description
metadata.parameters_json = parameters_json
metadata.status = status
metadata.author_staff_account_id = author_staff_account_id
metadata.author_display_name = author_display_name
if commit:
self.db.commit()
self.db.refresh(metadata)
else:
self.db.flush()
return metadata
def upsert_version_metadata(
self,
*,
draft_id: int,
tool_version_id: int,
tool_name: str,
display_name: str,
domain: str,
description: str,
parameters_json: list[dict],
version_number: int,
status: ToolLifecycleStatus,
author_staff_account_id: int,
author_display_name: str,
commit: bool = True,
) -> ToolMetadata:
existing = self.get_by_tool_version_id(tool_version_id)
if existing is None:
return self.create(
draft_id=draft_id,
tool_version_id=tool_version_id,
tool_name=tool_name,
display_name=display_name,
domain=domain,
description=description,
parameters_json=parameters_json,
version_number=version_number,
status=status,
author_staff_account_id=author_staff_account_id,
author_display_name=author_display_name,
commit=commit,
)
return self.update_metadata(
existing,
display_name=display_name,
domain=domain,
description=description,
parameters_json=parameters_json,
status=status,
author_staff_account_id=author_staff_account_id,
author_display_name=author_display_name,
commit=commit,
)
@staticmethod
def build_metadata_id(tool_name: str, version_number: int) -> str:
normalized_tool_name = str(tool_name or "").strip().lower()
return f"tool_metadata::{normalized_tool_name}::v{int(version_number)}"

@ -5,14 +5,25 @@ from dataclasses import dataclass
from datetime import UTC, datetime
from admin_app.core.settings import AdminSettings
from admin_app.db.models import ToolDraft, ToolVersion
from admin_app.db.models import ToolDraft, ToolMetadata, ToolVersion
from admin_app.db.models.tool_artifact import (
ToolArtifactKind,
ToolArtifactStage,
ToolArtifactStatus,
)
from admin_app.repositories.tool_artifact_repository import ToolArtifactRepository
from admin_app.repositories.tool_draft_repository import ToolDraftRepository
from admin_app.repositories.tool_metadata_repository import ToolMetadataRepository
from admin_app.repositories.tool_version_repository import ToolVersionRepository
from shared.contracts import (
GENERATED_TOOL_ENTRYPOINT,
GENERATED_TOOLS_PACKAGE,
ServiceName,
TOOL_LIFECYCLE_STAGES,
ToolLifecycleStatus,
ToolParameterType,
build_generated_tool_module_name,
build_generated_tool_module_path,
)
@ -196,6 +207,7 @@ _PARAMETER_TYPE_DESCRIPTIONS = {
_TOOL_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]{2,63}$")
_PARAMETER_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]{1,63}$")
_RESERVED_CORE_TOOL_NAMES = frozenset(entry.tool_name for entry in _BOOTSTRAP_TOOL_CATALOG)
class ToolManagementService:
@ -204,19 +216,26 @@ class ToolManagementService:
settings: AdminSettings,
draft_repository: ToolDraftRepository | None = None,
version_repository: ToolVersionRepository | None = None,
metadata_repository: ToolMetadataRepository | None = None,
artifact_repository: ToolArtifactRepository | None = None,
):
self.settings = settings
self.draft_repository = draft_repository
self.version_repository = version_repository
self.metadata_repository = metadata_repository
self.artifact_repository = artifact_repository
def build_overview_payload(self) -> dict:
catalog = self.list_publication_catalog()
catalog_payload = self.build_publications_payload()
catalog = catalog_payload["publications"]
persisted_draft_count = len(self.draft_repository.list_drafts()) if self.draft_repository else 0
persisted_version_count = 0
if self.version_repository is not None:
persisted_version_count = len(self.version_repository.list_versions())
elif self.draft_repository is not None:
persisted_version_count = sum(draft.version_count for draft in self.draft_repository.list_drafts())
persisted_metadata_count = len(self.metadata_repository.list_metadata()) if self.metadata_repository else 0
persisted_artifact_count = len(self.artifact_repository.list_artifacts()) if self.artifact_repository else 0
return {
"mode": "admin_tool_draft_governance",
"metrics": [
@ -224,7 +243,7 @@ class ToolManagementService:
"key": "active_catalog",
"label": "Tools mapeadas",
"value": str(len(catalog)),
"description": "Catalogo bootstrap refletindo a base de tools conhecida no monorepo.",
"description": "Catalogo governado persistido quando disponivel, com fallback bootstrap enquanto o admin ainda nao tiver metadados proprios.",
},
{
"key": "lifecycle_stages",
@ -250,6 +269,18 @@ class ToolManagementService:
"value": str(persisted_version_count),
"description": "Historico versionado das iteracoes de cada tool governada pelo admin.",
},
{
"key": "persisted_metadata",
"label": "Metadados persistidos",
"value": str(persisted_metadata_count),
"description": "Snapshots canonicos por versao com nome, descricao, parametros, status e autor da tool.",
},
{
"key": "persisted_artifacts",
"label": "Artefatos auditaveis",
"value": str(persisted_artifact_count),
"description": "Manifestos de geracao e relatorios de validacao gravados por versao para trilha administrativa.",
},
],
"workflow": self.build_lifecycle_payload(),
"next_steps": [
@ -315,6 +346,7 @@ class ToolManagementService:
],
"naming_rules": [
"tool_name deve usar snake_case minusculo, sem espacos, com 3 a 64 caracteres.",
"tool_name nao pode reutilizar nomes reservados pelo catalogo core ja publicado.",
"display_name deve explicar claramente a acao operacional que o bot vai executar.",
"Cada parametro precisa de nome, tipo, descricao e marcador de obrigatoriedade.",
],
@ -370,6 +402,17 @@ class ToolManagementService:
}
def build_publications_payload(self) -> dict:
metadata_entries = self._list_latest_metadata_entries()
if metadata_entries:
return {
"source": "admin_metadata_catalog",
"target_service": ServiceName.PRODUCT,
"publications": [
self._serialize_metadata_publication(metadata)
for metadata in metadata_entries
],
}
return {
"source": "bootstrap_catalog",
"target_service": ServiceName.PRODUCT,
@ -478,6 +521,33 @@ class ToolManagementService:
requires_director_approval=True,
)
if version is not None and self.metadata_repository is not None:
self.metadata_repository.upsert_version_metadata(
draft_id=draft.id,
tool_version_id=version.id,
tool_name=draft.tool_name,
display_name=draft.display_name,
domain=draft.domain,
description=draft.description,
parameters_json=stored_parameters,
version_number=version.version_number,
status=version.status,
author_staff_account_id=version.owner_staff_account_id,
author_display_name=version.owner_display_name,
)
if version is not None and self.artifact_repository is not None:
self._persist_initial_version_artifacts(
draft=draft,
version=version,
summary=summary,
warnings=warnings,
stored_parameters=stored_parameters,
required_parameter_count=required_parameter_count,
owner_staff_account_id=owner_staff_account_id,
owner_name=owner_name or "Autor administrativo",
)
return {
"storage_status": "admin_database",
"message": "Draft administrativo persistido com sucesso em fluxo versionado.",
@ -560,6 +630,150 @@ class ToolManagementService:
for entry in _BOOTSTRAP_TOOL_CATALOG
]
def _persist_initial_version_artifacts(
self,
*,
draft: ToolDraft,
version: ToolVersion,
summary: str,
warnings: list[str],
stored_parameters: list[dict],
required_parameter_count: int,
owner_staff_account_id: int,
owner_name: str,
) -> None:
if self.artifact_repository is None:
return
generation_payload = self._build_generation_artifact_payload(
draft=draft,
version=version,
summary=summary,
stored_parameters=stored_parameters,
)
validation_payload = self._build_validation_artifact_payload(
draft=draft,
version=version,
warnings=warnings,
stored_parameters=stored_parameters,
required_parameter_count=required_parameter_count,
)
self.artifact_repository.upsert_version_artifact(
draft_id=draft.id,
tool_version_id=version.id,
tool_name=draft.tool_name,
version_number=version.version_number,
artifact_stage=ToolArtifactStage.GENERATION,
artifact_kind=ToolArtifactKind.GENERATION_REQUEST,
artifact_status=ToolArtifactStatus.PENDING,
summary="Manifesto inicial de geracao persistido para auditoria da versao.",
payload_json=generation_payload,
author_staff_account_id=owner_staff_account_id,
author_display_name=owner_name,
)
self.artifact_repository.upsert_version_artifact(
draft_id=draft.id,
tool_version_id=version.id,
tool_name=draft.tool_name,
version_number=version.version_number,
artifact_stage=ToolArtifactStage.VALIDATION,
artifact_kind=ToolArtifactKind.VALIDATION_REPORT,
artifact_status=ToolArtifactStatus.SUCCEEDED,
summary="Relatorio de validacao do pre-cadastro persistido para auditoria da versao.",
payload_json=validation_payload,
author_staff_account_id=owner_staff_account_id,
author_display_name=owner_name,
)
@staticmethod
def _build_generation_artifact_payload(
*,
draft: ToolDraft,
version: ToolVersion,
summary: str,
stored_parameters: list[dict],
) -> dict:
return {
"source": "admin_draft_intake",
"tool_name": draft.tool_name,
"display_name": draft.display_name,
"domain": draft.domain,
"version_number": version.version_number,
"draft_id": draft.draft_id,
"version_id": version.version_id,
"business_goal": draft.business_goal,
"description": draft.description,
"summary": summary,
"parameters": list(stored_parameters),
"requires_director_approval": draft.requires_director_approval,
"target_package": GENERATED_TOOLS_PACKAGE,
"target_module": build_generated_tool_module_name(draft.tool_name),
"target_file_path": build_generated_tool_module_path(draft.tool_name),
"target_callable": GENERATED_TOOL_ENTRYPOINT,
"reserved_lifecycle_target": ToolLifecycleStatus.GENERATED.value,
}
@staticmethod
def _build_validation_artifact_payload(
*,
draft: ToolDraft,
version: ToolVersion,
warnings: list[str],
stored_parameters: list[dict],
required_parameter_count: int,
) -> dict:
return {
"source": "admin_draft_intake",
"tool_name": draft.tool_name,
"version_number": version.version_number,
"draft_id": draft.draft_id,
"version_id": version.version_id,
"validation_status": "passed",
"warnings": list(warnings),
"parameter_count": len(stored_parameters),
"required_parameter_count": required_parameter_count,
"checked_rules": [
"tool_name_snake_case",
"display_name_min_length",
"domain_catalog",
"description_min_length",
"business_goal_min_length",
"parameter_contracts",
],
}
def _list_latest_metadata_entries(self) -> list[ToolMetadata]:
if self.metadata_repository is None:
return []
latest_by_tool_name: dict[str, ToolMetadata] = {}
for metadata in self.metadata_repository.list_metadata():
normalized_tool_name = str(metadata.tool_name or "").strip().lower()
if normalized_tool_name in latest_by_tool_name:
continue
latest_by_tool_name[normalized_tool_name] = metadata
return list(latest_by_tool_name.values())
def _serialize_metadata_publication(self, metadata: ToolMetadata) -> dict:
parameters = self._serialize_parameters_for_response(metadata.parameters_json)
return {
"publication_id": metadata.metadata_id,
"tool_name": metadata.tool_name,
"display_name": metadata.display_name,
"description": metadata.description,
"domain": metadata.domain,
"version": metadata.version_number,
"status": metadata.status,
"parameter_count": len(parameters),
"parameters": parameters,
"author_name": metadata.author_display_name,
"implementation_module": build_generated_tool_module_name(metadata.tool_name),
"implementation_callable": GENERATED_TOOL_ENTRYPOINT,
"published_by": metadata.author_display_name,
"published_at": metadata.updated_at or metadata.created_at,
}
def _serialize_draft_summary(self, draft: ToolDraft) -> dict:
return {
"draft_id": draft.draft_id,
@ -654,6 +868,10 @@ class ToolManagementService:
tool_name = str(payload.get("tool_name") or "").strip().lower()
if not _TOOL_NAME_PATTERN.fullmatch(tool_name):
raise ValueError("tool_name deve usar snake_case minusculo com 3 a 64 caracteres.")
if tool_name in _RESERVED_CORE_TOOL_NAMES:
raise ValueError(
"tool_name reservado pelo catalogo core do sistema. Gere uma nova tool sem sobrescrever uma capability interna."
)
display_name = str(payload.get("display_name") or "").strip()
if len(display_name) < 4:

@ -224,7 +224,7 @@ function mountToolReviewBoard(board) {
setText("[data-tool-review-publication-count]", String(items.length));
setText("[data-tool-publication-source]", payload?.source || "Catalogo web");
publicationList.innerHTML = items.length > 0
? items.slice(0, 9).map((item) => `<div class="col-12 col-md-6 col-xxl-4"><article class="admin-tool-publication-card rounded-4 p-4 h-100"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.domain || "tool")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">v${escapeHtml(String(item.version || 1))}</span></div><p class="text-secondary mb-3">${escapeHtml(item.description || "Publicacao ativa no catalogo do produto.")}</p><div class="small text-secondary">${escapeHtml(item.implementation_module || "")}</div></article></div>`).join("")
? items.slice(0, 9).map((item) => `<div class="col-12 col-md-6 col-xxl-4"><article class="admin-tool-publication-card rounded-4 p-4 h-100"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.domain || "tool")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">v${escapeHtml(String(item.version || 1))}</span></div><p class="text-secondary mb-3">${escapeHtml(item.description || "Publicacao ativa no catalogo do produto.")}</p><div class="small text-secondary mb-1"><strong>Status:</strong> ${escapeHtml(item.status || "draft")}</div><div class="small text-secondary mb-1"><strong>Parametros:</strong> ${escapeHtml(String(item.parameter_count || 0))}</div><div class="small text-secondary mb-3"><strong>Autor:</strong> ${escapeHtml(item.author_name || item.published_by || "Nao informado")}</div><div class="small text-secondary">${escapeHtml(item.implementation_module || "")}</div></article></div>`).join("")
: `<div class="col-12"><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Catalogo ativo vazio</h4><p class="text-secondary mb-0">Nenhuma publicacao ativa retornada pela sessao web.</p></div></div>`;
}

@ -3,6 +3,8 @@ Rotina dedicada de bootstrap de banco de dados.
Cria tabelas e executa seed inicial de forma explicita, fora do startup do app.
"""
from pathlib import Path
from sqlalchemy import inspect, text
from app.core.settings import settings
@ -23,6 +25,22 @@ from app.db.mock_models import (
)
from app.db.mock_seed import seed_mock_data
from app.db.tool_seed import seed_tools
from shared.contracts import GENERATED_TOOLS_PACKAGE
_PROJECT_ROOT = Path(__file__).resolve().parents[2]
def _ensure_generated_tools_runtime_package() -> Path:
package_dir = _PROJECT_ROOT / GENERATED_TOOLS_PACKAGE
package_dir.mkdir(parents=True, exist_ok=True)
init_file = package_dir / "__init__.py"
if not init_file.exists():
init_file.write_text(
'"""Isolated runtime package for admin-governed generated tools."""\n',
encoding="utf-8",
)
return package_dir
def _ensure_mock_schema_evolution() -> None:
@ -56,6 +74,13 @@ def bootstrap_databases(
print("Inicializando bancos...")
failures: list[str] = []
try:
generated_tools_dir = _ensure_generated_tools_runtime_package()
print(f"Diretorio isolado de tools geradas pronto em {generated_tools_dir}.")
except Exception as exc:
print(f"Aviso: falha ao preparar diretorio isolado de tools geradas: {exc}")
failures.append(f"generated_tools={exc}")
should_seed_tools = settings.auto_seed_tools if run_tools_seed is None else bool(run_tools_seed)
should_seed_mock = (
settings.auto_seed_mock and settings.mock_seed_enabled

@ -2,6 +2,7 @@ import inspect
from typing import Callable, Dict, List
from fastapi import HTTPException
from shared.contracts import GENERATED_TOOL_ENTRYPOINT, GENERATED_TOOLS_PACKAGE
from sqlalchemy.orm import Session
from app.models.tool_model import ToolDefinition
@ -42,6 +43,10 @@ HANDLERS: Dict[str, Callable] = {
}
class GeneratedToolCoreBoundaryViolation(RuntimeError):
"""Raised when a generated tool attempts to reuse or point at core runtime code."""
# Registry em memoria das tools disponiveis para o orquestrador.
class ToolRegistry:
@ -66,6 +71,26 @@ class ToolRegistry:
def register_tool(self, name, description, parameters, handler):
"""Registra uma tool em memoria para uso pelo orquestrador."""
if self._is_generated_handler(handler):
self._ensure_generated_tool_boundary(name=name, handler=handler)
self._append_tool_definition(
name=name,
description=description,
parameters=parameters,
handler=handler,
)
def register_generated_tool(self, name, description, parameters, handler):
"""Registra uma tool gerada apenas quando ela respeita o pacote isolado do runtime."""
self._ensure_generated_tool_boundary(name=name, handler=handler)
self._append_tool_definition(
name=name,
description=description,
parameters=parameters,
handler=handler,
)
def _append_tool_definition(self, *, name, description, parameters, handler):
self._tools.append(
ToolDefinition(
name=name,
@ -75,6 +100,35 @@ class ToolRegistry:
)
)
@staticmethod
def _is_generated_handler(handler: Callable) -> bool:
module_name = str(getattr(handler, "__module__", "") or "").strip()
return module_name.startswith(f"{GENERATED_TOOLS_PACKAGE}.")
def _ensure_generated_tool_boundary(self, *, name: str, handler: Callable) -> None:
normalized_name = str(name or "").strip().lower()
if normalized_name in HANDLERS:
raise GeneratedToolCoreBoundaryViolation(
f"Tool gerada '{normalized_name}' nao pode sobrescrever um handler do catalogo core."
)
if any(str(tool.name or "").strip().lower() == normalized_name for tool in self._tools):
raise GeneratedToolCoreBoundaryViolation(
f"Tool gerada '{normalized_name}' nao pode sobrescrever uma tool ja registrada no runtime."
)
module_name = str(getattr(handler, "__module__", "") or "").strip()
if not module_name.startswith(f"{GENERATED_TOOLS_PACKAGE}."):
raise GeneratedToolCoreBoundaryViolation(
f"Tools geradas so podem ser carregadas do pacote isolado '{GENERATED_TOOLS_PACKAGE}.*'."
)
handler_name = str(getattr(handler, "__name__", "") or "").strip()
if handler_name != GENERATED_TOOL_ENTRYPOINT:
raise GeneratedToolCoreBoundaryViolation(
f"Tools geradas precisam expor o entrypoint governado '{GENERATED_TOOL_ENTRYPOINT}'."
)
def get_tools(self) -> List[ToolDefinition]:
"""Retorna a lista atual de tools registradas."""
return self._tools

@ -0,0 +1,3 @@
# Generated Tools
Diretorio isolado para modulos publicados pelo fluxo administrativo de tools.

@ -0,0 +1 @@
"""Isolated runtime package for admin-governed generated tools."""

@ -50,6 +50,8 @@ from shared.contracts.system_functional_configuration import (
get_functional_configuration,
)
from shared.contracts.tool_publication import (
GENERATED_TOOL_ENTRYPOINT,
GENERATED_TOOLS_PACKAGE,
PublishedToolContract,
ServiceName,
TOOL_LIFECYCLE_STAGES,
@ -59,12 +61,16 @@ from shared.contracts.tool_publication import (
ToolParameterContract,
ToolParameterType,
ToolPublicationEnvelope,
build_generated_tool_module_name,
build_generated_tool_module_path,
get_tool_lifecycle_stage,
)
__all__ = [
"AdminPermission",
"BOT_GOVERNED_SETTINGS",
"GENERATED_TOOL_ENTRYPOINT",
"GENERATED_TOOLS_PACKAGE",
"MODEL_RUNTIME_PROFILES",
"MODEL_RUNTIME_SEPARATION_RULES",
"PRODUCT_OPERATIONAL_DATASETS",
@ -103,6 +109,8 @@ __all__ = [
"FunctionalConfigurationMutability",
"FunctionalConfigurationPropagation",
"FunctionalConfigurationSource",
"build_generated_tool_module_name",
"build_generated_tool_module_path",
"get_bot_governed_setting",
"get_functional_configuration",
"get_model_runtime_contract",

@ -2,6 +2,7 @@ from __future__ import annotations
from datetime import datetime
from enum import Enum
import re
from pydantic import BaseModel, Field
@ -101,6 +102,28 @@ def get_tool_lifecycle_stage(
return _TOOL_LIFECYCLE_STAGE_BY_STATUS[normalized_status]
GENERATED_TOOLS_PACKAGE = "generated_tools"
GENERATED_TOOL_ENTRYPOINT = "run"
_GENERATED_TOOL_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]{2,63}$")
def _normalize_generated_tool_name(tool_name: str) -> str:
normalized = str(tool_name or "").strip().lower()
if not _GENERATED_TOOL_NAME_PATTERN.match(normalized):
raise ValueError("tool_name must use lowercase snake_case to build the generated module path.")
return normalized
def build_generated_tool_module_name(tool_name: str) -> str:
normalized = _normalize_generated_tool_name(tool_name)
return f"{GENERATED_TOOLS_PACKAGE}.{normalized}"
def build_generated_tool_module_path(tool_name: str) -> str:
normalized = _normalize_generated_tool_name(tool_name)
return f"{GENERATED_TOOLS_PACKAGE}/{normalized}.py"
class ToolParameterType(str, Enum):
STRING = "string"
INTEGER = "integer"

@ -9,9 +9,15 @@ from admin_app.api.dependencies import (
)
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from admin_app.db.models import ToolDraft, ToolVersion
from admin_app.db.models import ToolArtifact, ToolDraft, ToolMetadata, ToolVersion
from admin_app.db.models.tool_artifact import ToolArtifactKind
from admin_app.services import ToolManagementService
from shared.contracts import StaffRole, ToolLifecycleStatus
from shared.contracts import (
GENERATED_TOOL_ENTRYPOINT,
StaffRole,
ToolLifecycleStatus,
build_generated_tool_module_name,
)
class _FakeToolDraftRepository:
@ -76,7 +82,10 @@ class _FakeToolVersionRepository:
def list_versions(self, *, tool_name=None, draft_id=None, statuses=None) -> list[ToolVersion]:
versions = sorted(
self.versions,
key=lambda version: (version.version_number, version.updated_at or version.created_at or datetime.min.replace(tzinfo=timezone.utc)),
key=lambda version: (
version.version_number,
version.updated_at or version.created_at or datetime.min.replace(tzinfo=timezone.utc),
),
reverse=True,
)
if tool_name:
@ -113,12 +122,153 @@ class _FakeToolVersionRepository:
return f"tool_version::{normalized}::v{int(version_number)}"
class _FakeToolMetadataRepository:
def __init__(self):
self.metadata_entries: list[ToolMetadata] = []
self.next_id = 1
def list_metadata(self, *, tool_name=None, statuses=None) -> list[ToolMetadata]:
metadata_entries = sorted(
self.metadata_entries,
key=lambda metadata: (
metadata.version_number,
metadata.updated_at or metadata.created_at or datetime.min.replace(tzinfo=timezone.utc),
),
reverse=True,
)
if tool_name:
normalized = str(tool_name).strip().lower()
metadata_entries = [metadata for metadata in metadata_entries if metadata.tool_name == normalized]
if statuses:
allowed = set(statuses)
metadata_entries = [metadata for metadata in metadata_entries if metadata.status in allowed]
return metadata_entries
def get_by_tool_version_id(self, tool_version_id: int) -> ToolMetadata | None:
for metadata in self.metadata_entries:
if metadata.tool_version_id == tool_version_id:
return metadata
return None
def create(self, **kwargs) -> ToolMetadata:
version_number = kwargs["version_number"]
now = datetime(2026, 3, 31, 19, version_number, tzinfo=timezone.utc)
metadata = ToolMetadata(
id=self.next_id,
metadata_id=self.build_metadata_id(kwargs["tool_name"], version_number),
created_at=now,
updated_at=now,
**kwargs,
)
self.next_id += 1
self.metadata_entries.append(metadata)
return metadata
def update_metadata(self, metadata: ToolMetadata, **kwargs) -> ToolMetadata:
metadata.display_name = kwargs["display_name"]
metadata.domain = kwargs["domain"]
metadata.description = kwargs["description"]
metadata.parameters_json = kwargs["parameters_json"]
metadata.status = kwargs["status"]
metadata.author_staff_account_id = kwargs["author_staff_account_id"]
metadata.author_display_name = kwargs["author_display_name"]
metadata.updated_at = datetime(2026, 3, 31, 19, metadata.version_number, tzinfo=timezone.utc)
return metadata
def upsert_version_metadata(self, **kwargs) -> ToolMetadata:
existing = self.get_by_tool_version_id(kwargs["tool_version_id"])
if existing is None:
return self.create(**kwargs)
return self.update_metadata(existing, **kwargs)
@staticmethod
def build_metadata_id(tool_name: str, version_number: int) -> str:
normalized = str(tool_name or "").strip().lower()
return f"tool_metadata::{normalized}::v{int(version_number)}"
class _FakeToolArtifactRepository:
def __init__(self):
self.artifacts: list[ToolArtifact] = []
self.next_id = 1
def list_artifacts(self, *, tool_name=None, tool_version_id=None, artifact_stage=None, artifact_kind=None) -> list[ToolArtifact]:
artifacts = sorted(
self.artifacts,
key=lambda artifact: (
artifact.version_number,
artifact.updated_at or artifact.created_at or datetime.min.replace(tzinfo=timezone.utc),
),
reverse=True,
)
if tool_name:
normalized = str(tool_name).strip().lower()
artifacts = [artifact for artifact in artifacts if artifact.tool_name == normalized]
if tool_version_id is not None:
artifacts = [artifact for artifact in artifacts if artifact.tool_version_id == tool_version_id]
if artifact_stage:
artifacts = [artifact for artifact in artifacts if artifact.artifact_stage == artifact_stage]
if artifact_kind:
artifacts = [artifact for artifact in artifacts if artifact.artifact_kind == artifact_kind]
return artifacts
def get_by_tool_version_and_kind(self, tool_version_id: int, artifact_kind) -> ToolArtifact | None:
for artifact in self.artifacts:
if artifact.tool_version_id == tool_version_id and artifact.artifact_kind == artifact_kind:
return artifact
return None
def create(self, **kwargs) -> ToolArtifact:
version_number = kwargs["version_number"]
now = datetime(2026, 3, 31, 20, version_number, self.next_id, tzinfo=timezone.utc)
artifact = ToolArtifact(
id=self.next_id,
artifact_id=self.build_artifact_id(kwargs["tool_name"], version_number, kwargs["artifact_kind"]),
created_at=now,
updated_at=now,
checksum=kwargs.get("checksum") or f"fake-checksum-{self.next_id}",
**kwargs,
)
self.next_id += 1
self.artifacts.append(artifact)
return artifact
def update_artifact(self, artifact: ToolArtifact, **kwargs) -> ToolArtifact:
artifact.artifact_status = kwargs["artifact_status"]
artifact.storage_kind = kwargs["storage_kind"]
artifact.summary = kwargs["summary"]
artifact.payload_json = kwargs["payload_json"]
artifact.checksum = kwargs["checksum"]
artifact.author_staff_account_id = kwargs["author_staff_account_id"]
artifact.author_display_name = kwargs["author_display_name"]
artifact.updated_at = datetime(2026, 3, 31, 20, artifact.version_number, tzinfo=timezone.utc)
return artifact
def upsert_version_artifact(self, **kwargs) -> ToolArtifact:
existing = self.get_by_tool_version_and_kind(kwargs["tool_version_id"], kwargs["artifact_kind"])
if existing is None:
return self.create(**kwargs)
return self.update_artifact(existing, **kwargs)
@staticmethod
def build_artifact_id(tool_name: str, version_number: int, artifact_kind) -> str:
normalized = str(tool_name or "").strip().lower()
return f"tool_artifact::{normalized}::v{int(version_number)}::{artifact_kind.value}"
class AdminPanelToolsWebTests(unittest.TestCase):
def _build_client_with_role(
self,
role: StaffRole,
settings: AdminSettings | None = None,
) -> tuple[TestClient, object, _FakeToolDraftRepository, _FakeToolVersionRepository]:
) -> tuple[
TestClient,
object,
_FakeToolDraftRepository,
_FakeToolVersionRepository,
_FakeToolMetadataRepository,
_FakeToolArtifactRepository,
]:
app = create_app(
settings
or AdminSettings(
@ -128,10 +278,14 @@ class AdminPanelToolsWebTests(unittest.TestCase):
)
draft_repository = _FakeToolDraftRepository()
version_repository = _FakeToolVersionRepository()
metadata_repository = _FakeToolMetadataRepository()
artifact_repository = _FakeToolArtifactRepository()
service = ToolManagementService(
settings=app.state.admin_settings,
draft_repository=draft_repository,
version_repository=version_repository,
metadata_repository=metadata_repository,
artifact_repository=artifact_repository,
)
app.dependency_overrides[get_current_panel_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=21,
@ -141,10 +295,10 @@ class AdminPanelToolsWebTests(unittest.TestCase):
is_active=True,
)
app.dependency_overrides[get_tool_management_service] = lambda: service
return TestClient(app), app, draft_repository, version_repository
return TestClient(app), app, draft_repository, version_repository, metadata_repository, artifact_repository
def test_panel_tools_overview_is_available_for_colaborador_session(self):
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/panel/tools/overview")
finally:
@ -154,11 +308,37 @@ class AdminPanelToolsWebTests(unittest.TestCase):
payload = response.json()
self.assertEqual(payload["mode"], "admin_tool_draft_governance")
self.assertIn("persisted_versions", [item["key"] for item in payload["metrics"]])
self.assertIn("persisted_metadata", [item["key"] for item in payload["metrics"]])
self.assertIn("persisted_artifacts", [item["key"] for item in payload["metrics"]])
self.assertIn("/admin/panel/tools/contracts", [item["href"] for item in payload["actions"]])
self.assertIn("/admin/panel/tools/drafts/intake", [item["href"] for item in payload["actions"]])
def test_panel_tool_intake_persists_draft_with_version_metadata_for_colaborador(self):
client, app, draft_repository, version_repository = self._build_client_with_role(StaffRole.COLABORADOR)
def test_panel_tool_intake_blocks_tool_name_reserved_by_core_catalog_for_colaborador(self):
client, app, draft_repository, version_repository, metadata_repository, artifact_repository = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.post(
"/admin/panel/tools/drafts/intake",
json={
"domain": "vendas",
"tool_name": "consultar_estoque",
"display_name": "Consultar estoque paralelo",
"description": "Tentativa de sobrescrever a tool core de estoque com uma versao administrativa.",
"business_goal": "Validar que o painel bloqueia nomes reservados do runtime core.",
"parameters": [],
},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 422)
self.assertIn("catalogo core do sistema", response.json()["detail"])
self.assertEqual(len(draft_repository.drafts), 0)
self.assertEqual(len(version_repository.versions), 0)
self.assertEqual(len(metadata_repository.metadata_entries), 0)
self.assertEqual(len(artifact_repository.artifacts), 0)
def test_panel_tool_intake_persists_draft_with_version_metadata_and_artifacts_for_colaborador(self):
client, app, draft_repository, version_repository, metadata_repository, artifact_repository = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.post(
"/admin/panel/tools/drafts/intake",
@ -199,9 +379,13 @@ class AdminPanelToolsWebTests(unittest.TestCase):
self.assertEqual(len(payload["draft_preview"]["parameters"]), 2)
self.assertEqual(len(draft_repository.drafts), 1)
self.assertEqual(len(version_repository.versions), 1)
self.assertEqual(len(metadata_repository.metadata_entries), 1)
self.assertEqual(len(artifact_repository.artifacts), 2)
artifact_kinds = {artifact.artifact_kind for artifact in artifact_repository.artifacts}
self.assertEqual(artifact_kinds, {ToolArtifactKind.GENERATION_REQUEST, ToolArtifactKind.VALIDATION_REPORT})
def test_panel_drafts_list_returns_single_root_item_after_new_version(self):
client, app, draft_repository, version_repository = self._build_client_with_role(StaffRole.COLABORADOR)
client, app, draft_repository, version_repository, metadata_repository, artifact_repository = self._build_client_with_role(StaffRole.COLABORADOR)
try:
client.post(
"/admin/panel/tools/drafts/intake",
@ -238,9 +422,11 @@ class AdminPanelToolsWebTests(unittest.TestCase):
self.assertEqual(payload["drafts"][0]["version_count"], 2)
self.assertEqual(len(draft_repository.drafts), 1)
self.assertEqual(len(version_repository.versions), 2)
self.assertEqual(len(metadata_repository.metadata_entries), 2)
self.assertEqual(len(artifact_repository.artifacts), 4)
def test_panel_tools_review_queue_requires_director_session(self):
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/panel/tools/review-queue")
finally:
@ -253,7 +439,7 @@ class AdminPanelToolsWebTests(unittest.TestCase):
)
def test_panel_tools_review_queue_is_available_for_diretor_session(self):
client, app, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
response = client.get("/admin/panel/tools/review-queue")
finally:
@ -263,7 +449,7 @@ class AdminPanelToolsWebTests(unittest.TestCase):
self.assertEqual(response.json()["queue_mode"], "bootstrap_empty_state")
def test_panel_tools_publications_require_director_publication_permission(self):
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/panel/tools/publications")
finally:
@ -276,7 +462,7 @@ class AdminPanelToolsWebTests(unittest.TestCase):
)
def test_panel_tools_publications_return_catalog_for_diretor_session(self):
client, app, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
response = client.get("/admin/panel/tools/publications")
finally:
@ -284,10 +470,51 @@ class AdminPanelToolsWebTests(unittest.TestCase):
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["source"], "bootstrap_catalog")
self.assertEqual(payload["target_service"], "product")
self.assertGreaterEqual(len(payload["publications"]), 10)
self.assertIn("consultar_estoque", [item["tool_name"] for item in payload["publications"]])
def test_panel_tools_publications_prefer_persisted_metadata_for_diretor_session(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
intake_response = client.post(
"/admin/panel/tools/drafts/intake",
json={
"domain": "locacao",
"tool_name": "emitir_resumo_locacao",
"display_name": "Emitir resumo de locacao",
"description": "Resume contratos de locacao com filtros operacionais para o time interno.",
"business_goal": "Dar visibilidade rapida aos contratos e aos principais dados da locacao.",
"parameters": [
{
"name": "contrato_id",
"parameter_type": "string",
"description": "Identificador do contrato consultado.",
"required": True,
}
],
},
)
response = client.get("/admin/panel/tools/publications")
finally:
app.dependency_overrides.clear()
self.assertEqual(intake_response.status_code, 200)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["source"], "admin_metadata_catalog")
self.assertEqual(len(payload["publications"]), 1)
publication = payload["publications"][0]
self.assertEqual(publication["publication_id"], "tool_metadata::emitir_resumo_locacao::v1")
self.assertEqual(publication["tool_name"], "emitir_resumo_locacao")
self.assertEqual(publication["status"], "draft")
self.assertEqual(publication["implementation_module"], build_generated_tool_module_name("emitir_resumo_locacao"))
self.assertEqual(publication["implementation_callable"], GENERATED_TOOL_ENTRYPOINT)
self.assertEqual(publication["parameter_count"], 1)
self.assertEqual(publication["author_name"], "Equipe Web")
self.assertEqual(publication["parameters"][0]["name"], "contrato_id")
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,61 @@
import unittest
from admin_app.db.models import ToolArtifact
from admin_app.db.models.tool_artifact import (
ToolArtifactKind,
ToolArtifactStage,
ToolArtifactStatus,
ToolArtifactStorageKind,
)
class ToolArtifactModelTests(unittest.TestCase):
def test_tool_artifact_declares_expected_table_and_columns(self):
self.assertEqual(ToolArtifact.__tablename__, "tool_artifacts")
self.assertIn("artifact_id", ToolArtifact.__table__.columns)
self.assertIn("draft_id", ToolArtifact.__table__.columns)
self.assertIn("tool_version_id", ToolArtifact.__table__.columns)
self.assertIn("tool_name", ToolArtifact.__table__.columns)
self.assertIn("version_number", ToolArtifact.__table__.columns)
self.assertIn("artifact_stage", ToolArtifact.__table__.columns)
self.assertIn("artifact_kind", ToolArtifact.__table__.columns)
self.assertIn("artifact_status", ToolArtifact.__table__.columns)
self.assertIn("storage_kind", ToolArtifact.__table__.columns)
self.assertIn("summary", ToolArtifact.__table__.columns)
self.assertIn("payload_json", ToolArtifact.__table__.columns)
self.assertIn("checksum", ToolArtifact.__table__.columns)
self.assertIn("author_staff_account_id", ToolArtifact.__table__.columns)
self.assertIn("author_display_name", ToolArtifact.__table__.columns)
def test_tool_artifact_uses_expected_constraints_and_defaults(self):
draft_foreign_keys = list(ToolArtifact.__table__.columns["draft_id"].foreign_keys)
self.assertEqual(len(draft_foreign_keys), 1)
self.assertEqual(str(draft_foreign_keys[0].target_fullname), "tool_drafts.id")
version_foreign_keys = list(ToolArtifact.__table__.columns["tool_version_id"].foreign_keys)
self.assertEqual(len(version_foreign_keys), 1)
self.assertEqual(str(version_foreign_keys[0].target_fullname), "tool_versions.id")
author_foreign_keys = list(ToolArtifact.__table__.columns["author_staff_account_id"].foreign_keys)
self.assertEqual(len(author_foreign_keys), 1)
self.assertEqual(str(author_foreign_keys[0].target_fullname), "staff_accounts.id")
status_column = ToolArtifact.__table__.columns["artifact_status"]
self.assertEqual(status_column.default.arg, ToolArtifactStatus.PENDING)
self.assertEqual(status_column.type.process_bind_param("succeeded", None), "succeeded")
self.assertEqual(status_column.type.process_result_value("pending", None), ToolArtifactStatus.PENDING)
stage_column = ToolArtifact.__table__.columns["artifact_stage"]
kind_column = ToolArtifact.__table__.columns["artifact_kind"]
storage_column = ToolArtifact.__table__.columns["storage_kind"]
self.assertEqual(stage_column.type.process_bind_param("validation", None), "validation")
self.assertEqual(kind_column.type.process_result_value("generation_request", None), ToolArtifactKind.GENERATION_REQUEST)
self.assertEqual(storage_column.default.arg, ToolArtifactStorageKind.INLINE_JSON)
self.assertEqual(storage_column.type.process_result_value("inline_json", None), ToolArtifactStorageKind.INLINE_JSON)
unique_constraints = {constraint.name for constraint in ToolArtifact.__table__.constraints}
self.assertIn("uq_tool_artifacts_tool_version_kind", unique_constraints)
if __name__ == "__main__":
unittest.main()

@ -2,9 +2,17 @@ import unittest
from datetime import datetime, timezone
from admin_app.core import AdminSettings
from admin_app.db.models import ToolDraft, ToolVersion
from admin_app.db.models import ToolArtifact, ToolDraft, ToolMetadata, ToolVersion
from admin_app.db.models.tool_artifact import ToolArtifactKind
from admin_app.services.tool_management_service import ToolManagementService
from shared.contracts import ToolLifecycleStatus
from shared.contracts import (
GENERATED_TOOL_ENTRYPOINT,
GENERATED_TOOLS_PACKAGE,
ToolLifecycleStatus,
ToolParameterType,
build_generated_tool_module_name,
build_generated_tool_module_path,
)
class _FakeToolDraftRepository:
@ -116,7 +124,10 @@ class _FakeToolVersionRepository:
def list_versions(self, *, tool_name=None, draft_id=None, statuses=None) -> list[ToolVersion]:
versions = sorted(
self.versions,
key=lambda version: (version.version_number, version.updated_at or version.created_at or datetime.min.replace(tzinfo=timezone.utc)),
key=lambda version: (
version.version_number,
version.updated_at or version.created_at or datetime.min.replace(tzinfo=timezone.utc),
),
reverse=True,
)
if tool_name:
@ -179,17 +190,155 @@ class _FakeToolVersionRepository:
return f"tool_version::{normalized}::v{int(version_number)}"
class _FakeToolMetadataRepository:
def __init__(self):
self.metadata_entries: list[ToolMetadata] = []
self.next_id = 1
def list_metadata(self, *, tool_name=None, statuses=None) -> list[ToolMetadata]:
metadata_entries = sorted(
self.metadata_entries,
key=lambda metadata: (
metadata.version_number,
metadata.updated_at or metadata.created_at or datetime.min.replace(tzinfo=timezone.utc),
),
reverse=True,
)
if tool_name:
normalized = str(tool_name).strip().lower()
metadata_entries = [metadata for metadata in metadata_entries if metadata.tool_name == normalized]
if statuses:
allowed = set(statuses)
metadata_entries = [metadata for metadata in metadata_entries if metadata.status in allowed]
return metadata_entries
def get_by_tool_version_id(self, tool_version_id: int) -> ToolMetadata | None:
for metadata in self.metadata_entries:
if metadata.tool_version_id == tool_version_id:
return metadata
return None
def create(self, **kwargs) -> ToolMetadata:
version_number = kwargs["version_number"]
now = datetime(2026, 3, 31, 17, version_number, tzinfo=timezone.utc)
metadata = ToolMetadata(
id=self.next_id,
metadata_id=self.build_metadata_id(kwargs["tool_name"], version_number),
created_at=now,
updated_at=now,
**kwargs,
)
self.next_id += 1
self.metadata_entries.append(metadata)
return metadata
def update_metadata(self, metadata: ToolMetadata, **kwargs) -> ToolMetadata:
metadata.display_name = kwargs["display_name"]
metadata.domain = kwargs["domain"]
metadata.description = kwargs["description"]
metadata.parameters_json = kwargs["parameters_json"]
metadata.status = kwargs["status"]
metadata.author_staff_account_id = kwargs["author_staff_account_id"]
metadata.author_display_name = kwargs["author_display_name"]
metadata.updated_at = datetime(2026, 3, 31, 17, metadata.version_number, tzinfo=timezone.utc)
return metadata
def upsert_version_metadata(self, **kwargs) -> ToolMetadata:
existing = self.get_by_tool_version_id(kwargs["tool_version_id"])
if existing is None:
return self.create(**kwargs)
return self.update_metadata(existing, **kwargs)
@staticmethod
def build_metadata_id(tool_name: str, version_number: int) -> str:
normalized = str(tool_name or "").strip().lower()
return f"tool_metadata::{normalized}::v{int(version_number)}"
class _FakeToolArtifactRepository:
def __init__(self):
self.artifacts: list[ToolArtifact] = []
self.next_id = 1
def list_artifacts(self, *, tool_name=None, tool_version_id=None, artifact_stage=None, artifact_kind=None) -> list[ToolArtifact]:
artifacts = sorted(
self.artifacts,
key=lambda artifact: (
artifact.version_number,
artifact.updated_at or artifact.created_at or datetime.min.replace(tzinfo=timezone.utc),
),
reverse=True,
)
if tool_name:
normalized = str(tool_name).strip().lower()
artifacts = [artifact for artifact in artifacts if artifact.tool_name == normalized]
if tool_version_id is not None:
artifacts = [artifact for artifact in artifacts if artifact.tool_version_id == tool_version_id]
if artifact_stage:
artifacts = [artifact for artifact in artifacts if artifact.artifact_stage == artifact_stage]
if artifact_kind:
artifacts = [artifact for artifact in artifacts if artifact.artifact_kind == artifact_kind]
return artifacts
def get_by_tool_version_and_kind(self, tool_version_id: int, artifact_kind) -> ToolArtifact | None:
for artifact in self.artifacts:
if artifact.tool_version_id == tool_version_id and artifact.artifact_kind == artifact_kind:
return artifact
return None
def create(self, **kwargs) -> ToolArtifact:
version_number = kwargs["version_number"]
now = datetime(2026, 3, 31, 18, version_number, self.next_id, tzinfo=timezone.utc)
artifact = ToolArtifact(
id=self.next_id,
artifact_id=self.build_artifact_id(kwargs["tool_name"], version_number, kwargs["artifact_kind"]),
created_at=now,
updated_at=now,
checksum=kwargs.get("checksum") or f"fake-checksum-{self.next_id}",
**kwargs,
)
self.next_id += 1
self.artifacts.append(artifact)
return artifact
def update_artifact(self, artifact: ToolArtifact, **kwargs) -> ToolArtifact:
artifact.artifact_status = kwargs["artifact_status"]
artifact.storage_kind = kwargs["storage_kind"]
artifact.summary = kwargs["summary"]
artifact.payload_json = kwargs["payload_json"]
artifact.checksum = kwargs["checksum"]
artifact.author_staff_account_id = kwargs["author_staff_account_id"]
artifact.author_display_name = kwargs["author_display_name"]
artifact.updated_at = datetime(2026, 3, 31, 18, artifact.version_number, tzinfo=timezone.utc)
return artifact
def upsert_version_artifact(self, **kwargs) -> ToolArtifact:
existing = self.get_by_tool_version_and_kind(kwargs["tool_version_id"], kwargs["artifact_kind"])
if existing is None:
return self.create(**kwargs)
return self.update_artifact(existing, **kwargs)
@staticmethod
def build_artifact_id(tool_name: str, version_number: int, artifact_kind) -> str:
normalized = str(tool_name or "").strip().lower()
return f"tool_artifact::{normalized}::v{int(version_number)}::{artifact_kind.value}"
class AdminToolManagementServiceTests(unittest.TestCase):
def setUp(self):
self.draft_repository = _FakeToolDraftRepository()
self.version_repository = _FakeToolVersionRepository()
self.metadata_repository = _FakeToolMetadataRepository()
self.artifact_repository = _FakeToolArtifactRepository()
self.service = ToolManagementService(
settings=AdminSettings(admin_api_prefix="/admin"),
draft_repository=self.draft_repository,
version_repository=self.version_repository,
metadata_repository=self.metadata_repository,
artifact_repository=self.artifact_repository,
)
def test_create_draft_submission_persists_initial_tool_version(self):
def test_create_draft_submission_persists_initial_tool_version_metadata_and_artifacts(self):
payload = self.service.create_draft_submission(
{
"domain": "vendas",
@ -225,6 +374,56 @@ class AdminToolManagementServiceTests(unittest.TestCase):
self.assertEqual(payload["draft_preview"]["owner_name"], "Equipe Interna")
self.assertEqual(len(self.draft_repository.drafts), 1)
self.assertEqual(len(self.version_repository.versions), 1)
self.assertEqual(len(self.metadata_repository.metadata_entries), 1)
self.assertEqual(self.metadata_repository.metadata_entries[0].author_display_name, "Equipe Interna")
self.assertEqual(self.metadata_repository.metadata_entries[0].version_number, 1)
self.assertEqual(len(self.artifact_repository.artifacts), 2)
artifacts_by_kind = {
artifact.artifact_kind: artifact
for artifact in self.artifact_repository.artifacts
}
generation_artifact = artifacts_by_kind[ToolArtifactKind.GENERATION_REQUEST]
validation_artifact = artifacts_by_kind[ToolArtifactKind.VALIDATION_REPORT]
self.assertEqual(generation_artifact.tool_name, "consultar_vendas_periodo")
self.assertEqual(generation_artifact.payload_json["target_package"], GENERATED_TOOLS_PACKAGE)
self.assertEqual(
generation_artifact.payload_json["target_module"],
build_generated_tool_module_name("consultar_vendas_periodo"),
)
self.assertEqual(
generation_artifact.payload_json["target_file_path"],
build_generated_tool_module_path("consultar_vendas_periodo"),
)
self.assertEqual(generation_artifact.payload_json["target_callable"], GENERATED_TOOL_ENTRYPOINT)
self.assertEqual(generation_artifact.payload_json["reserved_lifecycle_target"], "generated")
self.assertEqual(generation_artifact.payload_json["parameters"][0]["name"], "periodo_inicio")
self.assertEqual(validation_artifact.payload_json["validation_status"], "passed")
self.assertEqual(validation_artifact.payload_json["parameter_count"], 2)
self.assertEqual(validation_artifact.author_display_name, "Equipe Interna")
self.assertTrue(generation_artifact.checksum)
self.assertTrue(validation_artifact.checksum)
def test_create_draft_submission_blocks_tool_name_reserved_by_core_catalog(self):
with self.assertRaisesRegex(ValueError, "catalogo core do sistema"):
self.service.create_draft_submission(
{
"domain": "vendas",
"tool_name": "consultar_estoque",
"display_name": "Consultar estoque paralelo",
"description": "Tentativa de sobrescrever a tool core de estoque com uma versao administrativa.",
"business_goal": "Validar que o admin nao permite reutilizar nomes reservados do runtime core.",
"parameters": [],
},
owner_staff_account_id=11,
owner_name="Equipe Interna",
)
self.assertEqual(len(self.draft_repository.drafts), 0)
self.assertEqual(len(self.version_repository.versions), 0)
self.assertEqual(len(self.metadata_repository.metadata_entries), 0)
self.assertEqual(len(self.artifact_repository.artifacts), 0)
def test_create_draft_submission_reuses_root_draft_and_increments_version(self):
self.service.create_draft_submission(
@ -265,6 +464,8 @@ class AdminToolManagementServiceTests(unittest.TestCase):
self.assertEqual(payload["draft_preview"]["version_count"], 2)
self.assertEqual(len(self.draft_repository.drafts), 1)
self.assertEqual(len(self.version_repository.versions), 2)
self.assertEqual(len(self.metadata_repository.metadata_entries), 2)
self.assertEqual(len(self.artifact_repository.artifacts), 4)
self.assertEqual(self.draft_repository.drafts[0].current_version_number, 2)
self.assertEqual(self.draft_repository.drafts[0].version_count, 2)
self.assertEqual(self.draft_repository.drafts[0].owner_display_name, "Coordenacao de Locacao")
@ -305,6 +506,43 @@ class AdminToolManagementServiceTests(unittest.TestCase):
self.assertEqual(payload["drafts"][0]["owner_name"], "Diretoria Comercial")
self.assertEqual(payload["supported_statuses"], [ToolLifecycleStatus.DRAFT])
def test_build_publications_payload_prefers_persisted_metadata_catalog(self):
self.service.create_draft_submission(
{
"domain": "revisao",
"tool_name": "consultar_revisao_aberta",
"display_name": "Consultar revisao aberta",
"description": "Consulta revisoes abertas com filtros administrativos para a oficina.",
"business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.",
"parameters": [
{
"name": "placa",
"parameter_type": "string",
"description": "Placa usada na busca da revisao.",
"required": True,
}
],
},
owner_staff_account_id=8,
owner_name="Operacao de Oficina",
)
payload = self.service.build_publications_payload()
self.assertEqual(payload["source"], "admin_metadata_catalog")
self.assertEqual(payload["target_service"], "product")
self.assertEqual(len(payload["publications"]), 1)
publication = payload["publications"][0]
self.assertEqual(publication["publication_id"], "tool_metadata::consultar_revisao_aberta::v1")
self.assertEqual(publication["tool_name"], "consultar_revisao_aberta")
self.assertEqual(publication["version"], 1)
self.assertEqual(publication["status"], ToolLifecycleStatus.DRAFT)
self.assertEqual(publication["author_name"], "Operacao de Oficina")
self.assertEqual(publication["implementation_module"], build_generated_tool_module_name("consultar_revisao_aberta"))
self.assertEqual(publication["implementation_callable"], GENERATED_TOOL_ENTRYPOINT)
self.assertEqual(publication["parameter_count"], 1)
self.assertEqual(publication["parameters"][0]["parameter_type"], ToolParameterType.STRING)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,49 @@
import unittest
from admin_app.db.models import ToolMetadata
from shared.contracts import ToolLifecycleStatus
class ToolMetadataModelTests(unittest.TestCase):
def test_tool_metadata_declares_expected_table_and_columns(self):
self.assertEqual(ToolMetadata.__tablename__, "tool_metadata")
self.assertIn("metadata_id", ToolMetadata.__table__.columns)
self.assertIn("draft_id", ToolMetadata.__table__.columns)
self.assertIn("tool_version_id", ToolMetadata.__table__.columns)
self.assertIn("tool_name", ToolMetadata.__table__.columns)
self.assertIn("display_name", ToolMetadata.__table__.columns)
self.assertIn("domain", ToolMetadata.__table__.columns)
self.assertIn("description", ToolMetadata.__table__.columns)
self.assertIn("parameters_json", ToolMetadata.__table__.columns)
self.assertIn("version_number", ToolMetadata.__table__.columns)
self.assertIn("status", ToolMetadata.__table__.columns)
self.assertIn("author_staff_account_id", ToolMetadata.__table__.columns)
self.assertIn("author_display_name", ToolMetadata.__table__.columns)
self.assertIn("created_at", ToolMetadata.__table__.columns)
self.assertIn("updated_at", ToolMetadata.__table__.columns)
def test_tool_metadata_uses_expected_constraints_and_defaults(self):
draft_foreign_keys = list(ToolMetadata.__table__.columns["draft_id"].foreign_keys)
self.assertEqual(len(draft_foreign_keys), 1)
self.assertEqual(str(draft_foreign_keys[0].target_fullname), "tool_drafts.id")
version_foreign_keys = list(ToolMetadata.__table__.columns["tool_version_id"].foreign_keys)
self.assertEqual(len(version_foreign_keys), 1)
self.assertEqual(str(version_foreign_keys[0].target_fullname), "tool_versions.id")
author_foreign_keys = list(ToolMetadata.__table__.columns["author_staff_account_id"].foreign_keys)
self.assertEqual(len(author_foreign_keys), 1)
self.assertEqual(str(author_foreign_keys[0].target_fullname), "staff_accounts.id")
status_column = ToolMetadata.__table__.columns["status"]
self.assertEqual(status_column.default.arg, ToolLifecycleStatus.DRAFT)
self.assertEqual(status_column.type.process_bind_param("archived", None), "archived")
self.assertEqual(status_column.type.process_result_value("draft", None), ToolLifecycleStatus.DRAFT)
unique_constraints = {constraint.name for constraint in ToolMetadata.__table__.constraints}
self.assertIn("uq_tool_metadata_tool_name_version_number", unique_constraints)
self.assertTrue(ToolMetadata.__table__.columns["tool_version_id"].unique)
if __name__ == "__main__":
unittest.main()

@ -6,9 +6,15 @@ from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_staff_principal, get_tool_management_service
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from admin_app.db.models import ToolDraft, ToolVersion
from admin_app.db.models import ToolArtifact, ToolDraft, ToolMetadata, ToolVersion
from admin_app.db.models.tool_artifact import ToolArtifactKind
from admin_app.services import ToolManagementService
from shared.contracts import StaffRole, ToolLifecycleStatus
from shared.contracts import (
GENERATED_TOOL_ENTRYPOINT,
StaffRole,
ToolLifecycleStatus,
build_generated_tool_module_name,
)
class _FakeToolDraftRepository:
@ -73,7 +79,10 @@ class _FakeToolVersionRepository:
def list_versions(self, *, tool_name=None, draft_id=None, statuses=None) -> list[ToolVersion]:
versions = sorted(
self.versions,
key=lambda version: (version.version_number, version.updated_at or version.created_at or datetime.min.replace(tzinfo=timezone.utc)),
key=lambda version: (
version.version_number,
version.updated_at or version.created_at or datetime.min.replace(tzinfo=timezone.utc),
),
reverse=True,
)
if tool_name:
@ -110,12 +119,153 @@ class _FakeToolVersionRepository:
return f"tool_version::{normalized}::v{int(version_number)}"
class _FakeToolMetadataRepository:
def __init__(self):
self.metadata_entries: list[ToolMetadata] = []
self.next_id = 1
def list_metadata(self, *, tool_name=None, statuses=None) -> list[ToolMetadata]:
metadata_entries = sorted(
self.metadata_entries,
key=lambda metadata: (
metadata.version_number,
metadata.updated_at or metadata.created_at or datetime.min.replace(tzinfo=timezone.utc),
),
reverse=True,
)
if tool_name:
normalized = str(tool_name).strip().lower()
metadata_entries = [metadata for metadata in metadata_entries if metadata.tool_name == normalized]
if statuses:
allowed = set(statuses)
metadata_entries = [metadata for metadata in metadata_entries if metadata.status in allowed]
return metadata_entries
def get_by_tool_version_id(self, tool_version_id: int) -> ToolMetadata | None:
for metadata in self.metadata_entries:
if metadata.tool_version_id == tool_version_id:
return metadata
return None
def create(self, **kwargs) -> ToolMetadata:
version_number = kwargs["version_number"]
now = datetime(2026, 3, 31, 18, version_number, tzinfo=timezone.utc)
metadata = ToolMetadata(
id=self.next_id,
metadata_id=self.build_metadata_id(kwargs["tool_name"], version_number),
created_at=now,
updated_at=now,
**kwargs,
)
self.next_id += 1
self.metadata_entries.append(metadata)
return metadata
def update_metadata(self, metadata: ToolMetadata, **kwargs) -> ToolMetadata:
metadata.display_name = kwargs["display_name"]
metadata.domain = kwargs["domain"]
metadata.description = kwargs["description"]
metadata.parameters_json = kwargs["parameters_json"]
metadata.status = kwargs["status"]
metadata.author_staff_account_id = kwargs["author_staff_account_id"]
metadata.author_display_name = kwargs["author_display_name"]
metadata.updated_at = datetime(2026, 3, 31, 18, metadata.version_number, tzinfo=timezone.utc)
return metadata
def upsert_version_metadata(self, **kwargs) -> ToolMetadata:
existing = self.get_by_tool_version_id(kwargs["tool_version_id"])
if existing is None:
return self.create(**kwargs)
return self.update_metadata(existing, **kwargs)
@staticmethod
def build_metadata_id(tool_name: str, version_number: int) -> str:
normalized = str(tool_name or "").strip().lower()
return f"tool_metadata::{normalized}::v{int(version_number)}"
class _FakeToolArtifactRepository:
def __init__(self):
self.artifacts: list[ToolArtifact] = []
self.next_id = 1
def list_artifacts(self, *, tool_name=None, tool_version_id=None, artifact_stage=None, artifact_kind=None) -> list[ToolArtifact]:
artifacts = sorted(
self.artifacts,
key=lambda artifact: (
artifact.version_number,
artifact.updated_at or artifact.created_at or datetime.min.replace(tzinfo=timezone.utc),
),
reverse=True,
)
if tool_name:
normalized = str(tool_name).strip().lower()
artifacts = [artifact for artifact in artifacts if artifact.tool_name == normalized]
if tool_version_id is not None:
artifacts = [artifact for artifact in artifacts if artifact.tool_version_id == tool_version_id]
if artifact_stage:
artifacts = [artifact for artifact in artifacts if artifact.artifact_stage == artifact_stage]
if artifact_kind:
artifacts = [artifact for artifact in artifacts if artifact.artifact_kind == artifact_kind]
return artifacts
def get_by_tool_version_and_kind(self, tool_version_id: int, artifact_kind) -> ToolArtifact | None:
for artifact in self.artifacts:
if artifact.tool_version_id == tool_version_id and artifact.artifact_kind == artifact_kind:
return artifact
return None
def create(self, **kwargs) -> ToolArtifact:
version_number = kwargs["version_number"]
now = datetime(2026, 3, 31, 19, version_number, self.next_id, tzinfo=timezone.utc)
artifact = ToolArtifact(
id=self.next_id,
artifact_id=self.build_artifact_id(kwargs["tool_name"], version_number, kwargs["artifact_kind"]),
created_at=now,
updated_at=now,
checksum=kwargs.get("checksum") or f"fake-checksum-{self.next_id}",
**kwargs,
)
self.next_id += 1
self.artifacts.append(artifact)
return artifact
def update_artifact(self, artifact: ToolArtifact, **kwargs) -> ToolArtifact:
artifact.artifact_status = kwargs["artifact_status"]
artifact.storage_kind = kwargs["storage_kind"]
artifact.summary = kwargs["summary"]
artifact.payload_json = kwargs["payload_json"]
artifact.checksum = kwargs["checksum"]
artifact.author_staff_account_id = kwargs["author_staff_account_id"]
artifact.author_display_name = kwargs["author_display_name"]
artifact.updated_at = datetime(2026, 3, 31, 19, artifact.version_number, tzinfo=timezone.utc)
return artifact
def upsert_version_artifact(self, **kwargs) -> ToolArtifact:
existing = self.get_by_tool_version_and_kind(kwargs["tool_version_id"], kwargs["artifact_kind"])
if existing is None:
return self.create(**kwargs)
return self.update_artifact(existing, **kwargs)
@staticmethod
def build_artifact_id(tool_name: str, version_number: int, artifact_kind) -> str:
normalized = str(tool_name or "").strip().lower()
return f"tool_artifact::{normalized}::v{int(version_number)}::{artifact_kind.value}"
class AdminToolsWebTests(unittest.TestCase):
def _build_client_with_role(
self,
role: StaffRole,
settings: AdminSettings | None = None,
) -> tuple[TestClient, object, _FakeToolDraftRepository, _FakeToolVersionRepository]:
) -> tuple[
TestClient,
object,
_FakeToolDraftRepository,
_FakeToolVersionRepository,
_FakeToolMetadataRepository,
_FakeToolArtifactRepository,
]:
app = create_app(
settings
or AdminSettings(
@ -125,10 +275,14 @@ class AdminToolsWebTests(unittest.TestCase):
)
draft_repository = _FakeToolDraftRepository()
version_repository = _FakeToolVersionRepository()
metadata_repository = _FakeToolMetadataRepository()
artifact_repository = _FakeToolArtifactRepository()
service = ToolManagementService(
settings=app.state.admin_settings,
draft_repository=draft_repository,
version_repository=version_repository,
metadata_repository=metadata_repository,
artifact_repository=artifact_repository,
)
app.dependency_overrides[get_current_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=11,
@ -138,10 +292,10 @@ class AdminToolsWebTests(unittest.TestCase):
is_active=True,
)
app.dependency_overrides[get_tool_management_service] = lambda: service
return TestClient(app), app, draft_repository, version_repository
return TestClient(app), app, draft_repository, version_repository, metadata_repository, artifact_repository
def test_tools_overview_returns_metrics_workflow_and_actions_for_colaborador(self):
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/tools/overview", headers={"Authorization": "Bearer token"})
finally:
@ -153,13 +307,15 @@ class AdminToolsWebTests(unittest.TestCase):
self.assertEqual(payload["mode"], "admin_tool_draft_governance")
self.assertEqual(payload["metrics"][0]["value"], "18")
self.assertIn("persisted_versions", [item["key"] for item in payload["metrics"]])
self.assertIn("persisted_metadata", [item["key"] for item in payload["metrics"]])
self.assertIn("persisted_artifacts", [item["key"] for item in payload["metrics"]])
self.assertIn("active", [item["code"] for item in payload["workflow"]])
self.assertIn("/admin/tools/contracts", [item["href"] for item in payload["actions"]])
self.assertIn("/admin/tools/drafts/intake", [item["href"] for item in payload["actions"]])
self.assertIn("artefatos", payload["next_steps"][0].lower())
def test_tools_contracts_return_shared_contract_snapshot(self):
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/tools/contracts", headers={"Authorization": "Bearer token"})
finally:
@ -176,8 +332,33 @@ class AdminToolsWebTests(unittest.TestCase):
self.assertIn("string", [item["code"] for item in payload["parameter_types"]])
self.assertIn("published_tool", payload["publication_fields"])
def test_tools_draft_intake_blocks_tool_name_reserved_by_core_catalog(self):
client, app, draft_repository, version_repository, metadata_repository, artifact_repository = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.post(
"/admin/tools/drafts/intake",
headers={"Authorization": "Bearer token"},
json={
"domain": "vendas",
"tool_name": "consultar_estoque",
"display_name": "Consultar estoque paralelo",
"description": "Tentativa de sobrescrever a tool core de estoque com uma versao administrativa.",
"business_goal": "Validar que a API bloqueia nomes reservados do runtime core.",
"parameters": [],
},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 422)
self.assertIn("catalogo core do sistema", response.json()["detail"])
self.assertEqual(len(draft_repository.drafts), 0)
self.assertEqual(len(version_repository.versions), 0)
self.assertEqual(len(metadata_repository.metadata_entries), 0)
self.assertEqual(len(artifact_repository.artifacts), 0)
def test_tools_drafts_return_single_root_draft_with_current_version_after_reintake(self):
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
first_response = client.post(
"/admin/tools/drafts/intake",
@ -218,8 +399,8 @@ class AdminToolsWebTests(unittest.TestCase):
self.assertEqual(payload["drafts"][0]["version_count"], 2)
self.assertEqual(payload["supported_statuses"], ["draft"])
def test_tools_draft_intake_persists_admin_draft_with_version_metadata(self):
client, app, draft_repository, version_repository = self._build_client_with_role(StaffRole.COLABORADOR)
def test_tools_draft_intake_persists_admin_draft_with_version_metadata_and_artifacts(self):
client, app, draft_repository, version_repository, metadata_repository, artifact_repository = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.post(
"/admin/tools/drafts/intake",
@ -256,9 +437,13 @@ class AdminToolsWebTests(unittest.TestCase):
self.assertGreaterEqual(len(payload["warnings"]), 1)
self.assertEqual(len(draft_repository.drafts), 1)
self.assertEqual(len(version_repository.versions), 1)
self.assertEqual(len(metadata_repository.metadata_entries), 1)
self.assertEqual(len(artifact_repository.artifacts), 2)
artifact_kinds = {artifact.artifact_kind for artifact in artifact_repository.artifacts}
self.assertEqual(artifact_kinds, {ToolArtifactKind.GENERATION_REQUEST, ToolArtifactKind.VALIDATION_REPORT})
def test_tools_review_queue_requires_director_review_permission(self):
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/tools/review-queue", headers={"Authorization": "Bearer token"})
finally:
@ -271,7 +456,7 @@ class AdminToolsWebTests(unittest.TestCase):
)
def test_tools_review_queue_is_available_for_diretor(self):
client, app, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
response = client.get("/admin/tools/review-queue", headers={"Authorization": "Bearer token"})
finally:
@ -284,7 +469,7 @@ class AdminToolsWebTests(unittest.TestCase):
self.assertIn("validated", payload["supported_statuses"])
def test_tools_publications_require_director_publication_permission(self):
client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
finally:
@ -297,7 +482,7 @@ class AdminToolsWebTests(unittest.TestCase):
)
def test_tools_publications_return_bootstrap_catalog_for_diretor(self):
client, app, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
finally:
@ -313,6 +498,48 @@ class AdminToolsWebTests(unittest.TestCase):
self.assertEqual(first["status"], "active")
self.assertEqual(first["implementation_module"], "app.services.tools.handlers")
def test_tools_publications_prefer_persisted_metadata_after_intake(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
intake_response = client.post(
"/admin/tools/drafts/intake",
headers={"Authorization": "Bearer token"},
json={
"domain": "revisao",
"tool_name": "consultar_revisao_aberta",
"display_name": "Consultar revisao aberta",
"description": "Consulta revisoes abertas com filtros administrativos para a oficina.",
"business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.",
"parameters": [
{
"name": "placa",
"parameter_type": "string",
"description": "Placa usada na busca da revisao.",
"required": True,
}
],
},
)
response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(intake_response.status_code, 200)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["source"], "admin_metadata_catalog")
self.assertEqual(len(payload["publications"]), 1)
publication = payload["publications"][0]
self.assertEqual(publication["publication_id"], "tool_metadata::consultar_revisao_aberta::v1")
self.assertEqual(publication["tool_name"], "consultar_revisao_aberta")
self.assertEqual(publication["status"], "draft")
self.assertEqual(publication["parameter_count"], 1)
self.assertEqual(publication["author_name"], "Equipe de Tools")
self.assertEqual(publication["implementation_module"], build_generated_tool_module_name("consultar_revisao_aberta"))
self.assertEqual(publication["implementation_callable"], GENERATED_TOOL_ENTRYPOINT)
self.assertEqual(publication["parameters"][0]["name"], "placa")
self.assertEqual(publication["parameters"][0]["parameter_type"], "string")
if __name__ == "__main__":
unittest.main()

@ -20,7 +20,15 @@ class AdminWriteGovernanceTests(unittest.TestCase):
self.assertEqual(payload["mode"], "admin_internal_tables_only")
self.assertEqual(
payload["allowed_direct_write_tables"],
["admin_audit_logs", "staff_accounts", "staff_sessions", "tool_drafts", "tool_versions"],
[
"admin_audit_logs",
"staff_accounts",
"staff_sessions",
"tool_drafts",
"tool_versions",
"tool_metadata",
"tool_artifacts",
],
)
self.assertIn("sales_orders", payload["blocked_operational_dataset_keys"])
self.assertIn("orders", payload["blocked_product_source_tables"])
@ -34,6 +42,8 @@ class AdminWriteGovernanceTests(unittest.TestCase):
ensure_direct_admin_write_allowed("admin_audit_logs")
ensure_direct_admin_write_allowed("tool_drafts")
ensure_direct_admin_write_allowed("tool_versions")
ensure_direct_admin_write_allowed("tool_metadata")
ensure_direct_admin_write_allowed("tool_artifacts")
def test_unknown_or_product_tables_raise_governance_violation(self):
with self.assertRaises(AdminWriteGovernanceViolation):
@ -44,7 +54,12 @@ class AdminWriteGovernanceTests(unittest.TestCase):
def test_session_guard_accepts_only_internal_admin_tables(self):
enforce_admin_session_write_governance(
new=(_FakeTabledObject("staff_accounts"), _FakeTabledObject("tool_versions")),
new=(
_FakeTabledObject("staff_accounts"),
_FakeTabledObject("tool_versions"),
_FakeTabledObject("tool_metadata"),
_FakeTabledObject("tool_artifacts"),
),
dirty=(_FakeTabledObject("staff_sessions"),),
deleted=(
_FakeTabledObject("admin_audit_logs"),

@ -16,7 +16,7 @@ from app.models.tool_model import ToolDefinition
from app.services.orchestration.conversation_policy import ConversationPolicy
from app.services.orchestration.entity_normalizer import EntityNormalizer
from app.services.orchestration.response_formatter import fallback_format_tool_result
from app.services.tools.tool_registry import ToolRegistry
from app.services.tools.tool_registry import GeneratedToolCoreBoundaryViolation, ToolRegistry
from app.services.tools.handlers import _parse_data_hora_revisao
@ -3648,5 +3648,59 @@ class ToolRegistryExecutionTests(unittest.IsolatedAsyncioTestCase):
)
def test_register_generated_tool_accepts_isolated_runtime_package(self):
async def run(**kwargs):
return kwargs
run.__module__ = "generated_tools.emitir_resumo_locacao"
registry = ToolRegistry.__new__(ToolRegistry)
registry._tools = []
registry.register_generated_tool(
name="emitir_resumo_locacao",
description="",
parameters={},
handler=run,
)
self.assertEqual(len(registry.get_tools()), 1)
self.assertEqual(registry.get_tools()[0].name, "emitir_resumo_locacao")
def test_register_generated_tool_blocks_core_name_collision(self):
async def run(**kwargs):
return kwargs
run.__module__ = "generated_tools.consultar_estoque"
registry = ToolRegistry.__new__(ToolRegistry)
registry._tools = []
with self.assertRaisesRegex(GeneratedToolCoreBoundaryViolation, "catalogo core"):
registry.register_generated_tool(
name="consultar_estoque",
description="",
parameters={},
handler=run,
)
def test_register_generated_tool_blocks_core_module_target(self):
async def run(**kwargs):
return kwargs
run.__module__ = "app.services.tools.handlers"
registry = ToolRegistry.__new__(ToolRegistry)
registry._tools = []
with self.assertRaisesRegex(GeneratedToolCoreBoundaryViolation, "generated_tools"):
registry.register_generated_tool(
name="emitir_resumo_locacao",
description="",
parameters={},
handler=run,
)
if __name__ == "__main__":
unittest.main()

@ -1,4 +1,6 @@
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import AsyncMock, patch
from app import main as main_module
@ -28,6 +30,37 @@ class SettingsParsingTests(unittest.TestCase):
class BootstrapRuntimeTests(unittest.TestCase):
@patch.object(bootstrap_module, "seed_tools")
@patch.object(bootstrap_module, "seed_mock_data")
@patch.object(bootstrap_module, "_ensure_mock_schema_evolution")
@patch.object(bootstrap_module.MockBase.metadata, "create_all")
@patch.object(bootstrap_module.Base.metadata, "create_all")
@patch.object(bootstrap_module, "_ensure_generated_tools_runtime_package")
def test_bootstrap_databases_ensures_generated_tools_package(
self,
ensure_generated_tools_runtime_package,
tools_create_all,
mock_create_all,
ensure_mock_schema_evolution,
seed_mock_data,
seed_tools,
):
ensure_generated_tools_runtime_package.return_value = Path("generated_tools")
bootstrap_module.bootstrap_databases()
ensure_generated_tools_runtime_package.assert_called_once_with()
tools_create_all.assert_called_once()
mock_create_all.assert_called_once()
def test_ensure_generated_tools_runtime_package_creates_package_files(self):
with TemporaryDirectory() as temp_dir:
with patch.object(bootstrap_module, "_PROJECT_ROOT", Path(temp_dir)):
package_dir = bootstrap_module._ensure_generated_tools_runtime_package()
self.assertEqual(package_dir.name, "generated_tools")
self.assertTrue(package_dir.exists())
self.assertTrue((package_dir / "__init__.py").exists())
@patch.object(bootstrap_module, "seed_tools")
@patch.object(bootstrap_module, "seed_mock_data")
@patch.object(bootstrap_module, "_ensure_mock_schema_evolution")

@ -4,6 +4,8 @@ from datetime import datetime, timezone
from shared.contracts import (
AdminPermission,
BOT_GOVERNED_SETTINGS,
GENERATED_TOOL_ENTRYPOINT,
GENERATED_TOOLS_PACKAGE,
MODEL_RUNTIME_PROFILES,
MODEL_RUNTIME_SEPARATION_RULES,
BotGovernanceArea,
@ -35,6 +37,8 @@ from shared.contracts import (
ToolParameterContract,
ToolParameterType,
ToolPublicationEnvelope,
build_generated_tool_module_name,
build_generated_tool_module_path,
get_bot_governed_setting,
get_tool_lifecycle_stage,
get_functional_configuration,
@ -121,6 +125,18 @@ class ToolPublicationContractTests(unittest.TestCase):
ToolParameterType.NUMBER,
)
def test_generated_tool_namespace_uses_isolated_package(self):
self.assertEqual(GENERATED_TOOLS_PACKAGE, "generated_tools")
self.assertEqual(GENERATED_TOOL_ENTRYPOINT, "run")
self.assertEqual(
build_generated_tool_module_name("consultar_financiamento"),
"generated_tools.consultar_financiamento",
)
self.assertEqual(
build_generated_tool_module_path("consultar_financiamento"),
"generated_tools/consultar_financiamento.py",
)
def test_lifecycle_catalog_exposes_expected_states_in_order(self):
self.assertEqual(
TOOL_LIFECYCLE_STATUS_SEQUENCE,

Loading…
Cancel
Save