🧩 feat(shared): definir contratos e deploy entre product e admin

feat/self-evolving-tools-foundation
parent 17583236a6
commit 1541948e76

@ -37,7 +37,6 @@ Capacidades de negocio ja implementadas:
- consultar frota de aluguel
- abrir locacao
- registrar pagamento de aluguel
- registrar multa de aluguel
- registrar devolucao de aluguel
- responder consultas informativas sobre o aluguel atual, como contrato, placa, diaria, pagamento e data de devolucao
@ -87,7 +86,7 @@ Importante:
## Estrutura do Projeto
```text
app/
app/ # runtime atual do produto
main.py
api/
schemas.py
@ -151,16 +150,56 @@ app/
user/
mock_customer_service.py
user_service.py
admin_app/ # novo runtime administrativo
app_factory.py
main.py
__main__.py
api/
dependencies.py
router.py
routes/
system.py
core/
settings.py
db/
models/
repositories/
services/
shared/ # contratos compartilhados entre servicos
contracts/
access_control.py
tool_publication.py
scripts/
list_integration_deliveries.py
list_integration_routes.py
process_integration_deliveries.py
upsert_integration_route.py
stress_smoke.py
docs/
adr/
architecture/
tests/
...
```
## Monorepo Em Evolucao
A partir da frente de auto-incremento, o repositorio passa a evoluir como monorepo com dois servicos:
- `app/`: runtime de produto e atendimento
- `admin_app/`: runtime administrativo para auth interna, configuracao, relatorios e governanca de tools
- `shared/`: contratos compartilhados entre os dois servicos
Nesta fase, o produto continua rodando a partir de `app/` sem migracao de import path.
A estrutura alvo detalhada esta em [docs/architecture/monorepo-target-structure.md](docs/architecture/monorepo-target-structure.md).
A hierarquia inicial de acesso e os primeiros contratos entre servicos estao em [docs/architecture/shared-contracts-and-access-hierarchy.md](docs/architecture/shared-contracts-and-access-hierarchy.md).
A estrategia de deploy independente entre os dois runtimes esta em [docs/architecture/independent-deploy-strategy.md](docs/architecture/independent-deploy-strategy.md).
## Tools Disponiveis
As definicoes padrao ficam em [app/db/tool_seed.py](app/db/tool_seed.py):
@ -180,7 +219,6 @@ As definicoes padrao ficam em [app/db/tool_seed.py](app/db/tool_seed.py):
| `consultar_frota_aluguel` | lista frota disponivel para locacao |
| `abrir_locacao_aluguel` | abre contrato de locacao |
| `registrar_pagamento_aluguel` | registra pagamento vinculado ao contrato |
| `registrar_multa_aluguel` | registra multa vinculada ao contrato |
| `registrar_devolucao_aluguel` | encerra locacao e calcula valor final |
| `limpar_contexto_conversa` | zera contexto e fila |
| `continuar_proximo_pedido` | retoma o proximo item da fila |
@ -398,6 +436,11 @@ No modelo atual:
- o bootstrap pode ser rodado manualmente ou por uma unit separada;
- em producao, Redis deve estar disponivel para o estado conversacional.
Para a topologia alvo com dois servicos, veja tambem:
- [docs/architecture/independent-deploy-strategy.md](docs/architecture/independent-deploy-strategy.md)
- [deploy/systemd/orquestrador-product.service.example](deploy/systemd/orquestrador-product.service.example)
- [deploy/systemd/orquestrador-admin.service.example](deploy/systemd/orquestrador-admin.service.example)
## Arquivos Uteis
- [TEST_CASES.md](TEST_CASES.md)
@ -413,3 +456,4 @@ Os proximos ganhos mais valiosos para o projeto sao:
- evoluir a trilha de auditoria e consultas operacionais
- criar uma camada de avaliacao semantica e replay de conversas
- integrar o orquestrador com sistemas reais de operacao

@ -0,0 +1,20 @@
[Unit]
Description=AI Orquestrador Admin Runtime
After=network.target
[Service]
Type=simple
User=vitor
Group=vitor
WorkingDirectory=/opt/orquestrador
EnvironmentFile=/opt/orquestrador/.env.admin
Environment=PATH=/opt/orquestrador/venv/bin
ExecStart=/opt/orquestrador/venv/bin/python -m uvicorn admin_app.main:app --host 127.0.0.1 --port 8081
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

@ -0,0 +1,20 @@
[Unit]
Description=AI Orquestrador Product Runtime
After=network.target
[Service]
Type=simple
User=vitor
Group=vitor
WorkingDirectory=/opt/orquestrador
EnvironmentFile=/opt/orquestrador/.env.product
Environment=PATH=/opt/orquestrador/venv/bin
ExecStart=/opt/orquestrador/venv/bin/python -m app.integrations.telegram_satellite_service
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

@ -0,0 +1,128 @@
# Estrategia De Deploy Independente Para Product E Admin
Este documento define a estrategia de deploy para manter `orquestrador-product`
e `orquestrador-admin` como servicos distintos, porem ligados.
## Objetivo
Permitir que:
- o atendimento continue estavel mesmo se o admin estiver fora do ar
- o admin evolua com login, painel, relatorios e geracao de tools sem impactar o hot path
- os dois servicos possam ser versionados e publicados com cadencias diferentes
## Servico de produto
Nome operacional sugerido:
- `orquestrador-product`
Responsabilidades:
- Telegram
- orquestracao
- execucao de tools publicadas
- regras operacionais do atendimento
Unit `systemd` sugerida:
- `deploy/systemd/orquestrador-product.service.example`
## Servico administrativo
Nome operacional sugerido:
- `orquestrador-admin`
Responsabilidades:
- autenticacao interna
- painel administrativo
- relatorios
- configuracao do sistema
- geracao, validacao, aprovacao e publicacao de tools
Unit `systemd` sugerida:
- `deploy/systemd/orquestrador-admin.service.example`
## Principios
1. Deploy do `admin` nao deve exigir restart do `product`.
2. Deploy do `product` nao deve depender do `admin` estar online.
3. Mudancas em `shared/contracts` devem ser compativeis para frente e para tras durante a janela de rollout.
4. O `product` consome somente estado publicado e aprovado.
5. O `admin` nao entra no hot path do atendimento.
## Configuracao de ambiente
Sugestao de arquivos distintos:
- `.env.product`
- `.env.admin`
### `product`
Mantem:
- Vertex do atendimento
- Redis do estado conversacional
- Telegram
- bancos do runtime operacional
### `admin`
Mantem:
- credenciais do painel interno
- banco administrativo
- modelo de geracao de codigo
- configuracoes de relatorios e publicacao
## Estrategia de rollout
### Mudancas so no admin
1. publicar codigo do `admin`
2. atualizar dependencias do `admin`
3. reiniciar apenas `orquestrador-admin`
### Mudancas so no product
1. publicar codigo do `product`
2. atualizar dependencias do `product`
3. reiniciar apenas `orquestrador-product`
### Mudancas em contratos compartilhados
1. publicar contrato novo de forma aditiva
2. subir primeiro o servico consumidor mais tolerante
3. subir o outro servico depois
4. so remover campos antigos numa fase posterior
## Banco e publicacao de estado
Nesta fase, a estrategia recomendada e:
- `admin` grava seus proprios metadados e artefatos
- `product` consome somente dados publicados e estaveis
- nenhuma dependencia sincrona do `product` para consultar `admin` em tempo de atendimento
## Observabilidade
Cada servico deve ter:
- logs proprios
- unit `systemd` propria
- variaveis de ambiente proprias
- healthcheck proprio
## Situacao atual
Hoje o runtime real em producao ainda e o de `product`.
Esta estrategia ja prepara o caminho para:
- manter o deploy atual do produto
- introduzir o `admin` como segundo servico
- fazer a transicao sem mover `app/` agora

@ -0,0 +1,147 @@
# Estrutura Alvo do Monorepo
Este documento define a estrutura alvo do monorepo apos a decisao de separar o runtime de produto do servico administrativo.
## Objetivo
Manter dois servicos distintos, mas ligados:
- `orquestrador-product`: atendimento e operacao do produto
- `orquestrador-admin`: autenticacao interna, configuracao, relatorios e governanca de tools
A prioridade desta fase e estrutural:
- preservar o runtime atual do produto
- introduzir o scaffold do servico administrativo
- criar um lugar claro para contratos compartilhados
## Decisao de transicao
Nesta etapa, o codigo atual do produto permanece em `app/`.
Nao vamos mover o runtime de produto agora para evitar churn desnecessario, quebra de import e risco operacional.
Portanto, a estrutura final de curto e medio prazo fica assim:
```text
app/ # runtime atual do produto
admin_app/ # novo runtime administrativo
shared/ # contratos e artefatos compartilhados entre servicos
docs/
adr/
architecture/
deploy/
systemd/
# evoluira para suportar servicos distintos
```
## Estrutura do servico de produto
O servico de produto continua centralizado em `app/`.
Responsabilidades:
- atendimento conversacional
- integracoes com canais externos
- orquestracao em tempo de execucao
- leitura de tools publicadas
- regras operacionais de vendas, revisao e locacao
## Estrutura do servico administrativo
O servico administrativo passa a nascer em `admin_app/`.
Estrutura alvo inicial:
```text
admin_app/
app_factory.py
main.py
__main__.py
api/
dependencies.py
router.py
routes/
system.py
# auth.py
# staff_accounts.py
# tool_drafts.py
# reports.py
core/
settings.py
# security.py
db/
models/
# staff_account.py
# tool_draft.py
# tool_generation_job.py
# tool_publication.py
# audit_log.py
repositories/
# staff_account_repository.py
# tool_draft_repository.py
services/
# auth_service.py
# tool_draft_service.py
# report_service.py
```
## Estrutura compartilhada
Tudo que for contrato entre servicos deve ficar em `shared/`.
Regras:
- `shared/` nao deve conter regra de negocio de atendimento
- `shared/` nao deve conter dependencias do hot path do Telegram
- `shared/` deve armazenar apenas contratos, DTOs, enums, nomes de eventos e utilitarios realmente compartilhados
Estrutura inicial:
```text
shared/
contracts/
access_control.py
tool_publication.py
# settings_snapshot.py
# report_filters.py
```
## Regras de organizacao do monorepo
1. `app/` continua sendo o produto ate eventual migracao planejada.
2. `admin_app/` nasce isolado e nao deve importar modulos internos de atendimento por conveniencia.
3. `shared/` e o unico lugar recomendado para contratos reutilizados por ambos os servicos.
4. O servico administrativo nao deve depender do runtime do Telegram para inicializar.
5. O servico de produto nao deve depender do servico administrativo no hot path.
6. Hierarquia de acesso administrativa deve nascer em shared/contracts/access_control.py.
## Import boundaries
### Permitido
- `admin_app` importar `shared`
- `app` importar `shared`
### Nao permitido
- `admin_app` importar `app.services.orchestration` para executar atendimento
- `app` importar `admin_app.services` no fluxo de atendimento
- colocar metadados administrativos dentro de `app/services/orchestration`
## Estrategia de evolucao
### Fase atual
- criar scaffold do `admin_app`
- criar `shared/`
- documentar a topologia do monorepo
### Fase seguinte
- implementar `StaffAccount`
- criar auth administrativa
- subir primeiras rotas internas no `admin_app`
### Fase posterior
- publicar contratos compartilhados em `shared/`
- plugar pipeline de drafts, validacao e publicacao de tools
## Impacto em deploy
Por enquanto, o deploy atual do produto permanece como esta.
Quando o admin ganhar runtime real, o deploy vai evoluir para dois servicos distintos:
- `orquestrador-product`
- `orquestrador-admin`
Sem mover o runtime atual de `app/` nesta etapa.

@ -0,0 +1,94 @@
# Contratos Compartilhados E Hierarquia De Acesso
Este documento define os primeiros contratos compartilhados entre `orquestrador-product`
e `orquestrador-admin`, com foco especial na hierarquia de acesso do runtime administrativo.
## Objetivo
Criar uma base comum para:
- autenticacao e autorizacao administrativa
- publicacao de tools do `admin` para o `product`
- evolucao independente dos dois servicos sem acoplamento indevido
## Hierarquia inicial de acesso
Os papeis administrativos ficam centralizados em `shared/contracts/access_control.py`.
Hierarquia:
1. `viewer`
2. `staff`
3. `admin`
### `viewer`
Responsavel por leitura operacional.
Permissoes iniciais:
- `view_system`
- `view_reports`
- `view_audit_logs`
### `staff`
Responsavel por operacao interna e governanca de drafts.
Permissoes iniciais:
- todas as de `viewer`
- `manage_tool_drafts`
- `review_tool_generations`
### `admin`
Responsavel por configuracao, publicacao e gestao de acesso.
Permissoes iniciais:
- todas as de `staff`
- `publish_tools`
- `manage_settings`
- `manage_staff_accounts`
## Regras de desenho
1. Os papeis nascem em contrato compartilhado para que `admin` e `product` falem a mesma lingua.
2. O `product` nao usa essa hierarquia para atendimento ao cliente final.
3. O `admin` usa essa hierarquia para autenticacao, autorizacao e auditoria.
4. Toda evolucao deve ser additive-first para nao bloquear deploy independente.
## Contrato de publicacao de tool
O contrato inicial fica em `shared/contracts/tool_publication.py`.
Ele cobre:
- `ServiceName`
- `ToolLifecycleStatus`
- `ToolParameterType`
- `ToolParameterContract`
- `PublishedToolContract`
- `ToolPublicationEnvelope`
## Como isso sera usado depois
### No `orquestrador-admin`
- criar `StaffAccount`
- associar `StaffAccount.role`
- controlar acesso a UI, as rotas e a aprovacao de tools
- emitir `ToolPublicationEnvelope` quando uma tool for publicada
### No `orquestrador-product`
- consumir apenas tools publicadas
- validar status e versao do contrato recebido
- evitar dependencia do runtime do admin no hot path
## Proximos passos naturais
- criar a entidade `StaffAccount`
- plugar a role do usuario interno ao contrato compartilhado
- modelar a persistencia de drafts/publicacoes de tool

@ -0,0 +1 @@
"""Contrato compartilhado entre servicos do monorepo."""

@ -0,0 +1,27 @@
# Shared Contracts
Esta pasta existe para concentrar contratos e artefatos compartilhados entre:
- `app/` (produto)
- `admin_app/` (administrativo)
Ela nao deve receber regra de negocio do atendimento nem codigo acoplado ao hot path do produto.
## Contratos iniciais
Nesta fase, os primeiros contratos compartilhados sao:
- `access_control.py`
- define a hierarquia inicial de acesso interno
- papeis: `viewer`, `staff`, `admin`
- permissoes iniciais para relatorios, configuracao, revisao e publicacao
- `tool_publication.py`
- define o contrato minimo de publicacao de tools do `admin` para o `product`
- inclui envelope de publicacao, status de ciclo de vida e schema de parametros
## Regras
- `shared/contracts` deve guardar apenas contratos estaveis entre servicos
- nada aqui deve importar modulos internos de `app/` ou `admin_app/`
- as mudancas devem ser additive-first para permitir deploy independente entre `product` e `admin`

@ -0,0 +1,31 @@
"""Contratos compartilhados entre product e admin."""
from shared.contracts.access_control import (
AdminPermission,
StaffRole,
permissions_for_role,
role_has_permission,
role_includes,
)
from shared.contracts.tool_publication import (
PublishedToolContract,
ServiceName,
ToolLifecycleStatus,
ToolParameterContract,
ToolParameterType,
ToolPublicationEnvelope,
)
__all__ = [
"AdminPermission",
"PublishedToolContract",
"ServiceName",
"StaffRole",
"ToolLifecycleStatus",
"ToolParameterContract",
"ToolParameterType",
"ToolPublicationEnvelope",
"permissions_for_role",
"role_has_permission",
"role_includes",
]

@ -0,0 +1,88 @@
from __future__ import annotations
from enum import Enum
class StaffRole(str, Enum):
VIEWER = "viewer"
STAFF = "staff"
ADMIN = "admin"
class AdminPermission(str, Enum):
VIEW_SYSTEM = "view_system"
VIEW_REPORTS = "view_reports"
VIEW_AUDIT_LOGS = "view_audit_logs"
MANAGE_TOOL_DRAFTS = "manage_tool_drafts"
REVIEW_TOOL_GENERATIONS = "review_tool_generations"
PUBLISH_TOOLS = "publish_tools"
MANAGE_SETTINGS = "manage_settings"
MANAGE_STAFF_ACCOUNTS = "manage_staff_accounts"
_ROLE_HIERARCHY = {
StaffRole.VIEWER: 10,
StaffRole.STAFF: 20,
StaffRole.ADMIN: 30,
}
_ROLE_PERMISSIONS = {
StaffRole.VIEWER: frozenset(
{
AdminPermission.VIEW_SYSTEM,
AdminPermission.VIEW_REPORTS,
AdminPermission.VIEW_AUDIT_LOGS,
}
),
StaffRole.STAFF: frozenset(
{
AdminPermission.VIEW_SYSTEM,
AdminPermission.VIEW_REPORTS,
AdminPermission.VIEW_AUDIT_LOGS,
AdminPermission.MANAGE_TOOL_DRAFTS,
AdminPermission.REVIEW_TOOL_GENERATIONS,
}
),
StaffRole.ADMIN: frozenset(
{
AdminPermission.VIEW_SYSTEM,
AdminPermission.VIEW_REPORTS,
AdminPermission.VIEW_AUDIT_LOGS,
AdminPermission.MANAGE_TOOL_DRAFTS,
AdminPermission.REVIEW_TOOL_GENERATIONS,
AdminPermission.PUBLISH_TOOLS,
AdminPermission.MANAGE_SETTINGS,
AdminPermission.MANAGE_STAFF_ACCOUNTS,
}
),
}
def normalize_staff_role(role: StaffRole | str) -> StaffRole:
if isinstance(role, StaffRole):
return role
return StaffRole(str(role).strip().lower())
def normalize_admin_permission(permission: AdminPermission | str) -> AdminPermission:
if isinstance(permission, AdminPermission):
return permission
return AdminPermission(str(permission).strip().lower())
def permissions_for_role(role: StaffRole | str) -> frozenset[AdminPermission]:
normalized_role = normalize_staff_role(role)
return _ROLE_PERMISSIONS[normalized_role]
def role_includes(role: StaffRole | str, minimum_role: StaffRole | str) -> bool:
normalized_role = normalize_staff_role(role)
normalized_minimum = normalize_staff_role(minimum_role)
return _ROLE_HIERARCHY[normalized_role] >= _ROLE_HIERARCHY[normalized_minimum]
def role_has_permission(
role: StaffRole | str, permission: AdminPermission | str
) -> bool:
normalized_permission = normalize_admin_permission(permission)
return normalized_permission in permissions_for_role(role)

@ -0,0 +1,59 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, Field
class ServiceName(str, Enum):
PRODUCT = "product"
ADMIN = "admin"
class ToolLifecycleStatus(str, Enum):
DRAFT = "draft"
GENERATED = "generated"
VALIDATED = "validated"
APPROVED = "approved"
ACTIVE = "active"
FAILED = "failed"
ARCHIVED = "archived"
class ToolParameterType(str, Enum):
STRING = "string"
INTEGER = "integer"
NUMBER = "number"
BOOLEAN = "boolean"
OBJECT = "object"
ARRAY = "array"
class ToolParameterContract(BaseModel):
name: str
parameter_type: ToolParameterType
description: str
required: bool = True
class PublishedToolContract(BaseModel):
tool_name: str
display_name: str
description: str
version: int = Field(ge=1)
status: ToolLifecycleStatus
parameters: tuple[ToolParameterContract, ...] = ()
implementation_module: str
implementation_callable: str
checksum: str | None = None
published_at: datetime | None = None
published_by: str | None = None
class ToolPublicationEnvelope(BaseModel):
source_service: ServiceName = ServiceName.ADMIN
target_service: ServiceName = ServiceName.PRODUCT
publication_id: str
published_tool: PublishedToolContract
emitted_at: datetime

@ -0,0 +1,82 @@
import unittest
from datetime import datetime, timezone
from shared.contracts import (
AdminPermission,
PublishedToolContract,
ServiceName,
StaffRole,
ToolLifecycleStatus,
ToolParameterContract,
ToolParameterType,
ToolPublicationEnvelope,
permissions_for_role,
role_has_permission,
role_includes,
)
class AccessControlContractTests(unittest.TestCase):
def test_role_hierarchy_is_ordered(self):
self.assertTrue(role_includes(StaffRole.ADMIN, StaffRole.STAFF))
self.assertTrue(role_includes(StaffRole.STAFF, StaffRole.VIEWER))
self.assertFalse(role_includes(StaffRole.VIEWER, StaffRole.ADMIN))
def test_permissions_are_inherited_by_higher_roles(self):
self.assertIn(
AdminPermission.VIEW_REPORTS,
permissions_for_role(StaffRole.VIEWER),
)
self.assertTrue(
role_has_permission(StaffRole.STAFF, AdminPermission.MANAGE_TOOL_DRAFTS)
)
self.assertTrue(
role_has_permission(StaffRole.ADMIN, AdminPermission.MANAGE_SETTINGS)
)
self.assertFalse(
role_has_permission(StaffRole.VIEWER, AdminPermission.PUBLISH_TOOLS)
)
class ToolPublicationContractTests(unittest.TestCase):
def test_tool_publication_envelope_is_built_with_shared_contract(self):
published_tool = PublishedToolContract(
tool_name="consultar_financiamento",
display_name="Consultar Financiamento",
description="Consulta opcoes de financiamento.",
version=2,
status=ToolLifecycleStatus.APPROVED,
parameters=(
ToolParameterContract(
name="valor_veiculo",
parameter_type=ToolParameterType.NUMBER,
description="Valor do veiculo em reais.",
required=True,
),
),
implementation_module="generated_tools.consultar_financiamento",
implementation_callable="run",
checksum="sha256:abc123",
published_at=datetime(2026, 3, 26, 12, 0, tzinfo=timezone.utc),
published_by="staff:1",
)
envelope = ToolPublicationEnvelope(
source_service=ServiceName.ADMIN,
target_service=ServiceName.PRODUCT,
publication_id="pub_001",
published_tool=published_tool,
emitted_at=datetime(2026, 3, 26, 12, 1, tzinfo=timezone.utc),
)
self.assertEqual(envelope.source_service, ServiceName.ADMIN)
self.assertEqual(envelope.target_service, ServiceName.PRODUCT)
self.assertEqual(envelope.published_tool.tool_name, "consultar_financiamento")
self.assertEqual(
envelope.published_tool.parameters[0].parameter_type,
ToolParameterType.NUMBER,
)
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save