feat: major architectural refactor to 5.1b1 - Service Layer, gRPC & Agent evolution (fragmented secrets)

This commit is contained in:
2026-04-17 18:42:08 -03:00
parent 85b23526cd
commit cb926c2b85
123 changed files with 38189 additions and 4640 deletions
+212
View File
@@ -0,0 +1,212 @@
# Remote Plugin Support — Implementation Plan
## Objetivo
Cuando connpy opera en modo remoto, el usuario puede usar plugins instalados **solo en el server**. La ejecución es completamente transparente: el cliente construye el argparse localmente (usando el source descargado del server) y todo lo demás corre en el server vía gRPC streaming.
---
## Arquitectura
```
Cliente Server
─────── ──────
connpy aws vpc vpc-123
├─ init() remoto
│ ├─ gRPC: list_plugins() → ["aws", "monitor"]
│ ├─ gRPC: get_plugin_source("aws") → aws.py source (texto)
│ ├─ Carga Parser en RAM → agrega subcomando al argparse
│ └─ Marca "aws" como remote_plugin
├─ argparse parsea args localmente (usa el Parser descargado)
└─ dispatch():
└─ "aws" es remote_plugin
└─ gRPC: invoke_plugin("aws", args_json) streaming
└─ Entrypoint(args, parser, connapp) ──→ output
←─── chunks de stdout/stderr ──┘
```
### Regla fundamental
- **`Parser`** → corre en el **cliente** (construye argparse)
- **`Entrypoint`** → corre en el **server** (toda la lógica del plugin)
- El plugin **no sabe** si está siendo ejecutado local o remotamente
---
## Cache de plugins remotos
### Estructura en disco
```
{configdir}/remote_plugins/
├── aws.py ← source descargado del server
```
### Cuándo se actualiza
Cada vez que `connpy` arranca en modo remoto, descarga y **sobreescribe** la cache en la ruta especificada por el archivo `.folder` activo. Sin hash, sin TTL, sin lógica extra. Siempre fresco.
### Uso por completion.py
`completion.py` incluye `{configdir}/remote_plugins/` como directorio adicional al escanear plugins. Carga `_connpy_tree()` desde el `.py` cacheado y automáticamente le inyecta la función `get_cwd()` para que pueda completar rutas locales sin dependencias extras.
---
## Gestión de plugin: `connpy plugin`
### `--list`: muestra local y remoto
```
connpy plugin --list
LOCAL:
aws [active]
tools [active]
REMOTE:
aws [shadowed] ← mismo nombre, local tiene prioridad
monitor [active]
deploy [active]
```
Estados posibles:
| Estado | Significado |
|---|---|
| `[active]` | Activo y usable |
| `[shadowed]` | Existe pero el otro lado tiene prioridad |
| `[disabled]` | Explícitamente desactivado |
### Prioridad cuando el mismo plugin existe en ambos lados
**Local gana por defecto.** Override guardado en:
```json
// ~/.config/conn/plugin_preferences.json
{
"aws": "remote"
}
```
### Semántica de `--enable`
Activa el plugin pedido. Si el mismo plugin existe en el otro lado → ese queda shadowed.
```bash
connpy plugin --enable aws # activa local, remoto queda shadowed
connpy plugin --enable aws --remote # activa remoto, local queda shadowed
```
### Semántica de `--disable`
Desactiva el plugin indicado. **NO activa automáticamente el del otro lado.**
Permite tener ambos desactivados si el usuario lo desea.
```bash
connpy plugin --disable aws # desactiva aws local (remoto no cambia)
connpy plugin --disable aws --remote # desactiva aws remoto (local no cambia)
```
### `--add`, `--del`, `--update`
```bash
connpy plugin --add myplug myfile.py # instala local
connpy plugin --add myplug myfile.py --remote # sube al server vía gRPC
connpy plugin --del myplug # borra local
connpy plugin --del myplug --remote # borra del server
connpy plugin --update myplug myfile.py # actualiza local
connpy plugin --update myplug myfile.py --remote # actualiza en server
```
---
## Archivos a modificar
| Archivo | Cambio |
|---|---|
| `grpc/connpy_pb2_grpc.py` | Agregar `get_plugin_source` + `invoke_plugin` a PluginService |
| `grpc/connpy_pb2.py` | Agregar `PluginInvokeRequest` + `OutputChunk` messages |
| `grpc/server.py` | Implementar métodos en `PluginServicer` |
| `grpc/stubs.py` | Agregar métodos a `PluginStub` |
| `services/plugin_service.py` | Agregar `get_plugin_source()` + `invoke_plugin()` |
| `connpy/plugins.py` | Remote loading, preferences, `remote_plugins` dict |
| `connapp.py` | Remote plugin init + dispatch proxy |
| `cli/plugin_handler.py` | Flag `--remote`, `--list` unificado, enable/disable con prefs |
| `completion.py` | Incluir cache remoto en `_get_plugins()` |
---
## Nuevo gRPC en `PluginService`
```python
# Mensajes nuevos
class PluginInvokeRequest:
name: str
args_json: str # argparse.Namespace serializado como JSON (solo tipos básicos)
class OutputChunk:
text: str
is_error: bool
# Métodos nuevos en PluginService
get_plugin_source(IdRequest) StringResponse
invoke_plugin(PluginInvokeRequest) stream OutputChunk
```
---
## Serialización de args
`argparse.Namespace` se serializa filtrando solo tipos básicos (str, int, float, bool, list, None):
```python
args_dict = {k: v for k, v in vars(args_namespace).items()
if isinstance(v, (str, int, float, bool, list, type(None)))}
```
**Limitación conocida**: plugins remotos no pueden usar `argparse.FileType`. Deben recibir paths como strings y abrir el archivo en el server.
---
## Flujo de enable/disable con conflictos
```
Estado inicial:
LOCAL: aws [active]
REMOTE: aws [shadowed]
connpy plugin --enable aws --remote
→ preferences: {"aws": "remote"}
LOCAL: aws [shadowed]
REMOTE: aws [active]
connpy plugin --disable aws --remote
→ gRPC disable en server, NO toca el local ni el pref
LOCAL: aws [shadowed] ← sigue con pref=remote pero remoto está disabled
REMOTE: aws [disabled]
connpy plugin --enable aws
→ Borra "aws" del preferences (vuelve a default = local)
LOCAL: aws [active]
REMOTE: aws [shadowed]
```
---
## Captura de output en el server
El server redirige `sys.stdout` durante la ejecución del `Entrypoint` y hace yield de cada línea:
```python
def invoke_plugin(self, name, args_dict):
import sys, io
from argparse import Namespace
args = Namespace(**args_dict)
old_stdout = sys.stdout
sys.stdout = buf = io.StringIO()
try:
plugin.Entrypoint(args, parser, connapp)
finally:
sys.stdout = old_stdout
for line in buf.getvalue().splitlines(keepends=True):
yield line
```
Si el plugin usa Rich o escribe directo al fd, se puede aislar en un subprocess.