🧩 feat(shared): definir contratos e deploy entre product e admin
parent
17583236a6
commit
1541948e76
@ -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,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…
Reference in New Issue