# Wrapper SSH do Assistente RH — instalação e operação

Implementa o contrato de `docs/03-CONTRATO-WRAPPER-CT107.md`, com as decisões de
`docs/07-DECISOES-FASE0.md` (1-4) a prevalecer sobre as notas "A CONFIRMAR" do
documento 03.

## Ficheiros

- `assistente-wrapper` — entrypoint bash invocado pelo `command=` do
  `authorized_keys`. Lê a classe de chave (`consulta`/`apply`) do argumento fixo
  do `command=`, JSON por stdin (com limite de 256 KB), chama `wrapper.php`.
- `wrapper.php` — dispatcher PHP 7.3. Valida a ação contra a classe de chave
  (autorização vem sempre da chave SSH usada, nunca do payload), resolve o
  ambiente (dev fixo / prod via `grep` inline ao `addclientback.php`), chama
  `inserir.php`/`portal_lookup.php`/`fichas_email.php`, devolve JSON sanitizado.
- `install.sh` — instalação idempotente (utilizador técnico, estrutura de
  diretórios, release versionada root-owned, `authorized_keys`, sudoers,
  pasta de idempotência).

## Ações implementadas

| Ação | Chave | Estado |
|---|---|---|
| `consulta.zonas` | consulta | Implementada (SELECT direto) |
| `consulta.reserva` | consulta | Implementada |
| `consulta.funcionario` | consulta | Implementada |
| `consulta.ficha` | consulta | Implementada (colunas do manifesto de `funcionarios` + id_processo/admissao/estado/email do último `processo`, por NIF, `REPLACE(nif,' ','')` dos dois lados; desde 23/09/2026 também `processo_contrato`, `funcionarios_condicoes` e `status_plataforma_rh.status` do processo — Fase 4b, para a conferência pós-inserção cobrir contrato/condições/App RH) |
| `consulta.clientes` | consulta | Implementada (23/09/2026, Fase 4b) — pesquisa `clientes` por `designacao_comercial`/`abreviatura` (LIKE, máx. 20), devolve `designacao_comercial`/`numero_contrato`/`zona`; usada pela sugestão de "Local de serviço" do Novo caso (SKILL.md passo 6) |
| `consulta.portal_lookup` | consulta | Implementada (chama `portal_lookup.php`) |
| `dry_run` | consulta | Implementada (chama `inserir.php` sem `--apply`) |
| `apply` | apply | Implementada, com idempotência por ficheiro `O_EXCL` |
| `verify` | consulta ou apply | Implementada (reusa `consulta.funcionario`) |
| `ficha_pdf` | apply | Implementada (Fase 4) — corre `ficha_pdf.php` (novo, release), que usa `funcoes/ficha_funcionario_render.php` + wkhtmltopdf e devolve o PDF em base64 (limite 10 MB) |
| `email` | apply | Implementada — `fichas_email.php --enviar`; `--ignora-modo-teste` **só em prod** (Fase 4: em dev confirma primeiro que `configuracoes_globais.email.modo_teste=1`, senão recusa o envio) |

## Instalação em DEV (CT 100, Sabichão dev)

Já instalado por este trabalho (ver secção "Estado em dev" abaixo). Para
reinstalar/atualizar:

```bash
# a partir do Mac, copiar os fontes para o CT 100
scp -i ~/.ssh/id_ed25519_claude \
  assistente-wrapper wrapper.php install.sh \
  root@192.168.69.2:/tmp/assistente-wrapper-src/    # depois: pct push 100 ...

# dentro do CT 100, com os .php da skill também presentes (SRC_SKILL):
ALVO=dev FROM_IP=192.168.69.150 \
PUB_CONSULTA="ssh-ed25519 AAAA...assistente-consulta@ct117" \
PUB_APPLY="ssh-ed25519 AAAA...assistente-apply@ct117" \
SRC_SKILL=/root/assistente-src \
  bash /root/assistente-wrapper-src/install.sh
```

`FROM_IP` é o IP do CT que faz as ligações — em dev, o CT 117
(`assistente-dev`, 192.168.69.150).

## Instalação em PROD (CT 107) — NÃO EXECUTADO, guia para quando for autorizado

**Nada disto foi corrido.** Passos exatos, a partir do Proxmox de prod
(10.0.30.10), quando o utilizador autorizar:

```bash
# 1. Copiar os fontes (wrapper + scripts da skill) para dentro do CT 107.
#    A partir do host 10.0.30.10:
pct push 107 /caminho/local/assistente-wrapper /root/assistente-wrapper-src/assistente-wrapper
pct push 107 /caminho/local/wrapper.php        /root/assistente-wrapper-src/wrapper.php
pct push 107 /caminho/local/install.sh         /root/assistente-wrapper-src/install.sh
pct push 107 /caminho/local/inserir.php        /root/assistente-src/inserir.php
pct push 107 /caminho/local/portal_lookup.php  /root/assistente-src/portal_lookup.php
pct push 107 /caminho/local/fichas_email.php   /root/assistente-src/fichas_email.php

# 2. Gerar (fora do CT 107, nesse momento) a chave de consulta de PRODUÇÃO —
#    nunca reusar a chave de consulta de dev nem a de apply de dev.
#    A chave de apply de prod só é criada na Fase 5 (decisão do doc 03),
#    não nesta instalação inicial.

# 3. Dentro do CT 107 (pct exec 107 -- bash), como root:
ALVO=prod FROM_IP=<IP do CT assistente de producao> \
PUB_CONSULTA="ssh-ed25519 AAAA...assistente-consulta-prod@..." \
SRC_SKILL=/root/assistente-src \
  bash /root/assistente-wrapper-src/install.sh

# (sem PUB_APPLY nesta fase — só instala a chave de consulta)

# 4. Confirmar manualmente:
#    - grep -m1 -oP "mysqli_connect\('localhost','segunor','\K[^']+" /var/www/sabichao/addclientback.php
#      resolve sem erro (o wrapper depende disto para dev != prod).
#    - authorized_keys de /opt/assistente/.ssh/ tem from= com o IP certo.
#    - sudoers -cf passa sem avisos.
#    - testar consulta.zonas a partir do CT assistente de produção.
```

### O que fica por fazer em prod

- Confirmar o IP real do CT assistente de produção (`FROM_IP` acima é um
  placeholder — usar o IP definitivo quando o CT existir).
- Gerar e instalar a chave de apply de prod, só na Fase 5, com aprovação
  explícita do utilizador (o doc 03 é claro: "só existe em prod a partir da
  Fase 5").
- Implementar `ficha_pdf` a sério (liga a `funcoes/ficha_funcionario_render.php`
  + wkhtmltopdf) antes de a chave de apply de prod poder ser usada para essa
  ação.
- O `install.sh` já instala o sudoers ativo (não só comentado) para
  `inserir.php`/`portal_lookup.php`/`fichas_email.php` correrem como
  `www-data` — necessário mesmo para `dry_run`/`consulta.portal_lookup`
  porque `funcoes/` do webroot é `0700 www-data:www-data` (confirmado em dev,
  ver secção de testes). Antes de instalar em prod, confirmar que a mesma
  pasta tem as mesmas permissões lá (é provável, mas não foi verificado —
  só leitura foi feita no CT 100 de dev) e que o `.env` de prod (0640
  `www-data:www-data`) também fica acessível pelo mesmo mecanismo quando
  `ficha_pdf`/`email`/`mark-concluded` forem implementados a sério.
- Rotação de chaves: seguir a decisão 13 (imediata em suspeita, anual no
  mínimo, responsável TI).
- Nunca copiar as chaves privadas de dev (`assistente_consulta`/`assistente_apply`
  em `~/.ssh` do CT 117) para prod — gerar sempre pares novos no CT
  assistente de produção quando existir.

## Estado em DEV (este trabalho, 2026-09-22)

- Utilizador técnico `assistente` criado no CT 100 (Sabichão dev,
  192.168.69.20 / `/var/www/develop`).
- Release instalada em `/opt/assistente/releases/<timestamp>`, symlink
  `/opt/assistente/bin/current`.
- `/opt/assistente/etc/alvo` = `dev`.
- `/var/lib/assistente/apply` criado, 0700, dono `assistente`.
- Duas chaves geradas no CT 117 (`assistente-dev`, 192.168.69.150, utilizador
  `claude`): `~/.ssh/assistente_consulta` e `~/.ssh/assistente_apply`
  (ed25519, sem passphrase). Instaladas ambas em dev (é dev, o doc 03 só proíbe
  a chave de apply em produção antes da Fase 5).
- `authorized_keys` do utilizador `assistente` no CT 100 com `from="192.168.69.150"`
  e `command=".../assistente-wrapper consulta"` / `"... apply"`.

## Testes reais feitos a partir do CT 117 (utilizador `claude`)

Todos os testes abaixo foram corridos de facto (não simulados) a partir do
CT 117 (`assistente-dev`, 192.168.69.150) contra o Sabichão de dev (CT 100,
192.168.69.20), 2026-09-22:

| Teste | Chave | Resultado observado | Exit |
|---|---|---|---|
| `consulta.zonas` | consulta | JSON com as 16 zonas ativas de dev | 0 |
| `consulta.reserva` NIF `999999990` (inexistente) | consulta | `{"reservas":[]}` | 0 |
| `dry_run` com manifesto sintético (NIF `123456789`, checksum válido, dados claramente fictícios) | consulta | Corre o `inserir.php` a sério contra a BD de dev; recusa corretamente por não haver reserva: `"sem reserva de processo para NIF 123456789 com admissao 2026-09-22"` | 1 |
| `apply` com o mesmo manifesto sintético | apply | Mesma recusa por falta de reserva (correto — a skill nunca cria reservas) | 1 |
| Repetição do `apply` acima, mesma `chave_idempotencia` | apply | Devolve o resultado gravado (`"idempotente_repetido":true`), sem tocar na BD outra vez | 1 |
| `apply` com o manifesto sintético | **consulta** | Recusado: `"acao nao permitida para esta chave"` | 1 |
| `ssh ... "bash"` / `ssh ... "id"` (tentativa de shell) | consulta | O `command=` do `authorized_keys` ignora o que o cliente pediu; o wrapper corre sempre, recebe stdin vazio, devolve `"payload vazio ou ilegivel"` | 1 |

Não foi criada nenhuma reserva sintética em dev para forçar um `apply`
bem-sucedido — a skill nunca cria reservas, e um `apply` sem reserva real é
precisamente o comportamento correto a testar.

### Dois problemas encontrados e corrigidos durante os testes (dev)

1. **StrictModes do sshd exige que o home do utilizador seja dono de si
   próprio.** `/opt/assistente` criado como `root:root` fazia o sshd recusar
   silenciosamente QUALQUER chave (mesmo sem `from=`/`command=`) com
   `"ED25519 key is not allowed"`, sem detalhe nenhum no log mesmo em
   `LogLevel DEBUG3`. Corrigido: `/opt/assistente` (só o home em si, não os
   subdiretórios de release) passa a `assistente:assistente`, `755` (não
   escrevível por outros, mas atravessável — o `www-data` precisa de entrar
   para ler os scripts da release via sudo).
2. **`dirname(__FILE__)` em PHP resolve o symlink `current` para o caminho
   real da release**, e o sudoers do `install.sh` autoriza o caminho estável
   do symlink — com o caminho real, o `sudo` não encontrava regra nenhuma e
   falhava silenciosamente com `"sudo: a password is required"` (sem nada no
   log de auditoria). Corrigido: `wrapper.php` usa a constante fixa
   `/opt/assistente/bin/current` em vez de `dirname(__FILE__)`.

Também descoberto (não um bug, mas relevante para o passo de prod): a pasta
`funcoes/` do webroot é `0700 www-data:www-data` **em dev também**, não só em
prod — por isso o wrapper corre sempre `inserir.php`/`portal_lookup.php`/
`fichas_email.php` via `sudo -u www-data`, mesmo para `dry_run`/`consulta.*`,
e não só para as ações que tocam no `.env` como o doc 03 previa inicialmente.
