Controle do host (arquivos, comandos, energia)¶
Seu serviço roda dentro do WSL e precisa agir na máquina que o hospeda:
ler um arquivo em C:\Users\..., rodar um comando PowerShell, abrir o
seletor de arquivos nativo, desligar o computador no fim do expediente.
tempest_fastapi_sdk.hostbridge é essa camada — a mesma que antes vivia
como um serviço separado em cada projeto que precisava dela.
Nada aqui é montado sozinho: create_app não conhece este módulo, e o
router existe porque um serviço pediu.
O sistema, os arquivos e os comandos não precisam de extra nenhum — é
biblioteca padrão e FastAPI. Ler PDF precisa de [pdf-read], e ler com
layout e tabelas precisa de [pdf-layout].
Isto é um shell naquela máquina
Quem alcança estes métodos roda comando como o usuário do host e desliga
o computador. Duas consequências, e nenhuma é opcional:
allowed_base_paths começa vazio — negando todo caminho até alguém
configurar — e make_hostbridge_router exige dependências de auth,
recusando uma lista vazia. O lado que escreve (comandos, escrita,
remoção, energia) fica atrás de destructive=True e vem desligado.
A primeira leitura¶
HostBridge recebe um HostBridgeConfig e nada mais. Tudo é async, e o
que toca disco ou processo roda fora do event loop:
import asyncio
from tempest_fastapi_sdk.hostbridge import HostBridge, HostBridgeConfig
async def main() -> None:
bridge = HostBridge(
HostBridgeConfig(allowed_base_paths=("/mnt/c/Users/me/Documents",)),
)
info = await bridge.host_info()
print(info.computer_name, info.os_version)
file = await bridge.read_text("C:\\Users\\me\\Documents\\notas.txt")
print(file.size_bytes, file.content[:80])
asyncio.run(main())
Repare no caminho: ele foi escrito na forma Windows e funcionou. O
bridge aceita as duas — C:\Users\me e /mnt/c/Users/me — e normaliza
antes de checar, com wslpath quando ele existe.
A ordem importa: resolver primeiro, checar depois
O caminho é resolvido (.. colapsado, link simbólico seguido) antes
de ser comparado com allowed_base_paths. Checar antes de resolver é o
bug clássico: /mnt/c/Users/me/../../../etc/passwd começa com uma base
permitida e termina fora dela.
As configurações, vindas do ambiente¶
Em um serviço, o lugar dessas opções é o Settings. Componha o mixin e
splat o mapper — a tradução campo → argumento mora num lugar só:
from tempest_fastapi_sdk.settings import HostBridgeSettings, ServerSettings
from tempest_fastapi_sdk.hostbridge import HostBridge, HostBridgeConfig
class Settings(HostBridgeSettings, ServerSettings):
"""Só o que é específico do serviço fica aqui."""
settings = Settings()
bridge = HostBridge(HostBridgeConfig(**settings.hostbridge_kwargs()))
As variáveis de ambiente são os próprios nomes dos campos:
HOST_ALLOWED_BASE_PATHS, HOST_POWERSHELL_BINARY, HOST_CMD_BINARY,
HOST_COMMAND_TIMEOUT e HOST_MAX_FILE_READ_BYTES.
HOST_ALLOWED_BASE_PATHS não tem default permissivo
Vazio nega tudo. É a direção segura de falhar: um bridge que pode rodar PowerShell não deveria também poder ler o disco inteiro só porque ninguém lembrou de configurar.
Expondo como HTTP¶
make_hostbridge_router devolve um APIRouter pronto para
include_router. Ele exige a lista de dependências — é a assinatura que
força a decisão de auth, em vez de um aviso na documentação:
from fastapi import Depends, FastAPI
from tempest_fastapi_sdk import make_token_dependency, register_exception_handlers
from tempest_fastapi_sdk.hostbridge import (
HostBridge,
HostBridgeConfig,
make_hostbridge_router,
)
app = FastAPI()
register_exception_handlers(app)
bridge = HostBridge(
HostBridgeConfig(allowed_base_paths=("/mnt/c/Users/me/Documents",)),
)
app.include_router(
make_hostbridge_router(
bridge,
dependencies=[Depends(make_token_dependency("um-segredo-longo"))],
)
)
Isso monta o lado que lê:
| Método | Rota | O que faz |
|---|---|---|
GET |
/system/info |
Nome da máquina, usuário, SO, uptime |
GET |
/system/files |
Lê um arquivo de texto |
GET |
/system/files/list |
Lista um diretório |
GET |
/system/files/pdf |
Extrai um PDF, página a página |
POST |
/system/files/pick |
Abre o seletor de arquivos nativo |
O lado que escreve — POST /system/exec, POST/DELETE
/system/files, POST /system/{shutdown,restart,abort,lock,logoff} — só
aparece com destructive=True:
from fastapi import Depends
from tempest_fastapi_sdk import make_token_dependency
from tempest_fastapi_sdk.hostbridge import (
HostBridge,
HostBridgeConfig,
make_hostbridge_router,
)
bridge = HostBridge(
HostBridgeConfig(allowed_base_paths=("/mnt/c/Users/me/Documents",)),
)
router = make_hostbridge_router(
bridge,
dependencies=[Depends(make_token_dependency("um-segredo-longo"))],
destructive=True,
)
Ligue o lado destrutivo só com alguém para confirmar
Um assistente que executa comandos sem confirmação humana é um assistente que apaga a pasta errada uma vez só. O padrão que funciona é segurar a chamada até o usuário aprovar, mostrando os argumentos exatos.
Os erros, com código¶
Toda falha sobe como exceção do envelope do SDK, então um serviço que já
chama register_exception_handlers responde {detail, code, details} sem
fiação extra — e com a frase em PT-BR ou en-US conforme o Accept-Language:
| Código | Status | Quando |
|---|---|---|
HOST_INVALID_PATH |
400 | Caminho fora das bases, ou intraduzível |
HOST_FILE_NOT_FOUND |
404 | Não existe arquivo ali |
HOST_FILE_TOO_LARGE |
413 | Acima de max_file_read_bytes |
HOST_FILE_DECODE_FAILED |
400 | Bytes não decodificam — um PDF ou imagem |
HOST_COMMAND_FAILED |
500 | O comando saiu com status diferente de zero |
HOST_COMMAND_TIMEOUT |
504 | O comando passou do tempo e foi morto |
HOST_UNAVAILABLE |
503 | O executável que a ação chama (PowerShell, wslpath, shutdown.exe) não existe aqui — basta faltar um |
HOST_UNAVAILABLE é o que separa "esta máquina não tem host Windows" de
"a ação falhou": num container Linux puro, o chamador degrada em vez de
relatar erro.
Lendo um PDF do host¶
import asyncio
from tempest_fastapi_sdk.hostbridge import HostBridge, HostBridgeConfig
from tempest_fastapi_sdk.pdf import PdfExtractor
async def main() -> None:
bridge = HostBridge(
HostBridgeConfig(allowed_base_paths=("/mnt/c/Users/me/Documents",)),
)
document = await bridge.read_pdf(
"C:\\Users\\me\\Documents\\contrato.pdf",
extractor=PdfExtractor.LAYOUT,
)
for page in document.pages:
print(page.page_number, len(page.tables), page.text[:60])
asyncio.run(main())
PdfExtractor.TEXT é pypdf — rápido, prosa corrida, extra [pdf-read].
PdfExtractor.LAYOUT é pdfplumber, extra [pdf-layout]: mais lento, mas
recupera colunas e preenche page.tables.
Não há OCR aqui
Página escaneada é imagem sem camada de texto: ela volta com text=""
em vez de sumir — a entrada n é sempre a página n. Cheque isso e
mande esses arquivos para outro lugar; entregar página vazia a um modelo
é como se inventa uma resposta confiante sobre algo que ninguém leu.
Rodando comandos¶
import asyncio
from tempest_fastapi_sdk.hostbridge import (
CommandSchema,
HostBridge,
HostBridgeConfig,
)
async def main() -> None:
bridge = HostBridge(HostBridgeConfig())
result = await bridge.run_command(
CommandSchema(command="Get-Date -Format o", shell="powershell", timeout=10),
)
print(result.return_code, result.stdout.strip(), result.duration_seconds)
asyncio.run(main())
Elevação (UAC) não acontece daqui
Um comando que precisa de administrador falha. Passe por gsudo ou por
uma entrada pré-criada no Agendador de Tarefas do lado Windows.
Recap¶
HostBridge+HostBridgeConfigdão a superfície inteira;HostBridgeSettingsé a mesma coisa vinda do ambiente, viahostbridge_kwargs().allowed_base_pathsvazio nega tudo, e o caminho é resolvido antes de ser checado.make_hostbridge_routerexige auth por assinatura, e só monta o lado que escreve comdestructive=True.- Toda falha carrega um
code—HOST_UNAVAILABLEavisa que esta máquina não tem host para controlar. - PDF:
[pdf-read]para texto,[pdf-layout]para colunas e tabelas, sem OCR em nenhum dos dois.