Metadata-Version: 2.4
Name: detector-paquetes-fantasma
Version: 0.1.0
Summary: Servidor MCP para verificar dependencias npm y PyPI de forma deterministica.
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11.4
Requires-Dist: fastmcp==3.4.4
Requires-Dist: httpx==0.28.1
Requires-Dist: packaging==26.2
Requires-Dist: pydantic==2.13.4
Requires-Dist: rapidfuzz==3.14.3
Requires-Dist: semantic-version==2.10.0
Requires-Dist: tree-sitter-javascript==0.25.0
Requires-Dist: tree-sitter==0.26.0
Provides-Extra: bedrock
Requires-Dist: boto3==1.43.56; extra == 'bedrock'
Provides-Extra: rag
Requires-Dist: sentence-transformers==5.6.1; extra == 'rag'
Description-Content-Type: text/markdown

# Detector de Paquetes Fantasma

Servidor MCP que verifica dependencias npm y PyPI antes de instalarlas. Responde una
sola pregunta con evidencia: ¿este paquete existe, es el que crees y trae algo
conocido en su contra?

El veredicto es determinista. Dos ejecuciones con los mismos datos devuelven el mismo
JSON, byte a byte: los pesos viven congelados en `rules_v1`, los identificadores de
finding derivan de su contenido y no hay relojes ni UUID aleatorios en la respuesta.

## Uso directo con uvx

```bash
uvx detector-paquetes-fantasma
```

El comando arranca el servidor sobre stdio. No necesita instalación previa, no
descarga modelos y no toca la red hasta que una herramienta lo pide.

Para probar el paquete construido localmente:

```bash
python -m build --no-isolation
uvx --from dist/detector_paquetes_fantasma-0.1.0-py3-none-any.whl detector-paquetes-fantasma
```

## Configuración en Kiro

Copia este bloque en `.kiro/settings/mcp.json`. Es el mismo contenido versionado en
`src/detector_paquetes_fantasma/resources/kiro/mcp.json`, y `scripts/release/check_rc.py`
falla si ambos divergen.

```json
{
  "mcpServers": {
    "detector-paquetes-fantasma": {
      "command": "uvx",
      "args": ["detector-paquetes-fantasma"],
      "transportType": "stdio",
      "env": {},
      "disabled": false,
      "autoApprove": ["verify_package", "verify_manifest", "explain_risk"]
    }
  }
}
```

El hook `PostFileSave` versionado en `.kiro/hooks/` verifica `package.json` y
`requirements.txt` al guardarlos. Nunca cancela el guardado: si el servidor no está,
tarda o responde algo inválido, avisa y sigue.

## Herramientas

### `verify_package(ecosystem, name, version=None)`

Resuelve la versión contra el registry oficial y devuelve un `ScanResult`.

```json
{
  "ecosystem": "npm",
  "name": "aws-sdk-mcp-helper"
}
```

```json
{
  "schema_version": 1,
  "request": {
    "ecosystem": "npm",
    "original_name": "aws-sdk-mcp-helper",
    "normalized_name": "aws-sdk-mcp-helper",
    "requested_specifier": null
  },
  "status": "COMPLETE",
  "resolved_version": null,
  "decision": {
    "rules_version": "rules_v1",
    "score": 70,
    "severity": "HIGH",
    "action": "BLOCK",
    "critical_flags": ["PACKAGE_NOT_FOUND"]
  },
  "findings": [
    {
      "type": "package_not_found",
      "source": "REGISTRY",
      "confidence": "CONFIRMED",
      "message": "el registry oficial afirma que el paquete no existe"
    }
  ]
}
```

`version` acepta una versión exacta, un rango o nada. Con rango se resuelve la mayor
estable publicada y se avisa que un lockfile puede elegir otra. Un specifier que no se
puede resolver (`workspace:*`, `file:../x`, referencias VCS) no se ignora: se reporta
como `unverifiable_entry` sin llamar al registry.

### `verify_manifest(ecosystem, manifest_content, path=None)`

Verifica todas las entradas de un `package.json` o `requirements.txt`.
`manifest_content` se trata siempre como dato: no se interpola en ningún comando.

```json
{
  "ecosystem": "npm",
  "manifest_content": "{\"dependencies\": {\"expres\": \"4.17.1\", \"lodash\": \"4.17.20\"}}",
  "path": "package.json"
}
```

La respuesta es un `ManifestScanResult` con un `ScanResult` por paquete,
`unverifiable_entries` para lo que no se pudo verificar y un `aggregate_decision` que
toma la acción más restrictiva, el score máximo y la unión estable de flags. Si falla
una dependencia, las demás se conservan.

### `explain_risk(result)`

Explica un `ScanResult` o `ManifestScanResult` ya calculado. La explicación jamás
modifica score, severidad, acción ni flags: `immutable_decision` es una copia exacta de
la decisión recibida. Devuelve la narrativa, el motor usado (`deterministic_template`,
`lexical_fallback`, `local_rag` o `bedrock`) y los casos históricos relacionados del
corpus versionado.

## Decisión y score

| Score | Severidad | Acción |
| --- | --- | --- |
| cualquier flag crítico | HIGH | BLOCK |
| 50–69 | MEDIUM | REVIEW |
| 20–49 | LOW | CAUTION |
| 0–19 | MINIMAL | ALLOW |

Los cuatro flags críticos son `PACKAGE_NOT_FOUND`, `OSV_ADVISORY_CONFIRMED`,
`TYPOSQUAT_CONFIRMED` y `STATIC_COMPOSITE_CONFIRMED`. No suman puntos: imponen un piso
de 70, más 10 por cada tipo crítico distinto adicional. Sin flags críticos el score
nunca pasa de 69, así que una acumulación de señales débiles no puede fabricar un
BLOCK. Cada tipo aporta una sola vez: repetir el mismo mensaje no infla el resultado.

## Configuración tipada

Todo se lee del entorno y se valida al arrancar. Un valor no interpretable produce un
error con la variable responsable, no un default silencioso.

| Variable | Default | Qué controla |
| --- | --- | --- |
| `DPF_OFFLINE` | `false` | prohíbe cualquier salida de red |
| `DPF_CACHE_PATH` | `detector-paquetes-fantasma.sqlite3` | ruta de la base SQLite |
| `DPF_REGISTRY_TTL_HOURS` | `168` | frescura de metadata de registry |
| `DPF_OSV_TTL_HOURS` | `6` | frescura de advisories |
| `DPF_HTTP_TIMEOUT_SECONDS` | `10` | timeout por request |
| `DPF_DOWNLOAD_TIMEOUT_SECONDS` | `30` | timeout de descarga de artefactos |
| `DPF_MAX_CONCURRENCY` | `8` | requests simultáneas al registry |
| `DPF_MAX_DOWNLOAD_BYTES` | `25000000` | tamaño máximo descargado |
| `DPF_MAX_ARCHIVE_FILES` | `2000` | entradas máximas por archivo |
| `DPF_MAX_FILE_BYTES` | `5000000` | tamaño máximo por archivo extraído |
| `DPF_MAX_EXPANDED_BYTES` | `50000000` | tamaño máximo expandido |
| `DPF_RAG_MODEL_PATH` | sin valor | ruta local del modelo de embeddings |
| `DPF_BEDROCK_ENABLED` | `false` | activa la redacción con Bedrock |
| `AWS_REGION` | sin valor | región requerida si Bedrock está activo |

Las credenciales nunca se persisten ni aparecen en logs: la configuración se serializa
redactada.

## Cache y modo offline

La cache es SQLite en modo WAL, con TTL separados para registry y OSV, y negative
caching solo cuando la ausencia es autoritativa (un 404 del registry). Un 5xx o un
timeout nunca se cachean como "no existe".

Con `DPF_OFFLINE=true` no se crea ni el cliente HTTP. Las respuestas se sirven de cache
declarando su edad en lenguaje llano (`info de hace 6h`) y bajando la cobertura a
`PARTIAL` o `UNVERIFIABLE`. Reutilizar dato viejo puede elevar la cautela, y esa
degradación se declara en los findings en lugar de fingir un veredicto completo.

## Explicaciones: local por defecto, Bedrock opcional

El orden de fallback es Bedrock → RAG local → recuperación léxica → plantilla
determinista. Sin configuración adicional se usa la recuperación léxica sobre el corpus
de incidentes versionado, y toda explicación sin Bedrock se etiqueta como
`generada localmente, sin Bedrock`.

- RAG con embeddings: instala el extra `rag` y apunta `DPF_RAG_MODEL_PATH` a un modelo
  local. El servidor nunca descarga pesos por su cuenta y el import de
  `sentence-transformers` es lazy: arrancar MCP no lo carga.
- Bedrock: instala el extra `bedrock`, define `DPF_BEDROCK_ENABLED=true` y `AWS_REGION`.
  El routing es determinista, Sonnet para scores 40–69 o señales contradictorias y Haiku
  para el resto. El payload lleva findings normalizados y procedencia, nunca código
  fuente completo, contenido de artefactos ni credenciales. Una respuesta que intente
  traer score, severidad, acción o flags se rechaza entera.

## Límites del análisis estático

El análisis estático es una señal, no una sentencia. Es AST de Python y tree-sitter para
JavaScript sobre el artefacto oficial, sin ejecutar nada y sin sandbox.

- No hay ejecución: no detecta comportamiento que solo aparece en runtime.
- El código ofuscado, minificado o generado dinámicamente puede evadirlo.
- Un finding aislado (`automatic_execution_isolated`, `sensitive_access_isolated`,
  `network_or_spawn_isolated`) se marca `solo detección estática, sin confirmación
  externa` y nunca bloquea por sí solo. Solo el composite confirmado es crítico.
- Si ningún parser puede analizar un archivo, se reporta `parser_failure` con el aviso
  de revisión manual; no se asume que sea limpio.

## Scripts y reportes

```bash
python -m scripts.probes.cache_offline           # cache, TTL y offline sin sockets
python -m scripts.scenarios.hallucinated_package # paquete alucinado
python -m scripts.scenarios.vulnerable_typo_manifest
python -m scripts.scenarios.offline_cache_replay
python -m benchmarks.full_manifest               # manifest completo, cache y concurrencia
python -m scripts.reports.scan_secrets           # reportes sin credenciales ni rutas personales
python -m scripts.reports.acceptance             # agregado en reports/acceptance.json
python -m scripts.release.check_rc               # verificación del release candidate
```

Los tests usan providers mockeados. La red queda reservada a los tests marcados `live`,
que solo corren con `--live` o `RUN_LIVE_TESTS=1`.

## Roadmap

- Sandboxing del análisis de artefactos.
- MCP remoto en Lambda.
- Redis como cache compartida.
- OpenTelemetry para trazas y métricas.
