feat(browser): funciones idempotentes para config de sistema de chromium

Cierra el gap de reproducibilidad entre PCs del proyecto web_scraping:
la organizacion de extensiones y el CDP global dejaban de ser pasos
manuales con sudo documentados en prosa.

- apply_chromium_extension_policy: escribe ExtensionInstallForcelist
  (whitelist via --keep) en /etc/chromium/policies/managed/extensions.json
  de forma idempotente, con backup automatico y validacion JSON. --dry-run
  previsualiza sin tocar el sistema.
- apply_chromium_cdp_flag: gestiona /etc/chromium.d/cdp (CDP global).
  Loopback por defecto, --network para bind 0.0.0.0 (con aviso), --remove
  para desactivar, --dry-run para previsualizar. Idempotente con backup.

Ambas: dominio browser, grupo navegator, impuras (escriben en /etc via
sudo), guard de auto-ejecucion (ejecutables con fn run y sourceables).

Docs del proyecto (CONVENTIONS.md reglas 8/9, CHROMIUM_SYSTEM.md
inventario + tabla accionable) ahora apuntan a 'fn run apply_chromium_*'
como metodo canonico en vez de editar los archivos de /etc a mano.
This commit is contained in:
Egutierrez
2026-06-05 16:33:35 +02:00
parent ccfa5bc78b
commit 830f2d34de
4 changed files with 547 additions and 0 deletions
@@ -0,0 +1,71 @@
---
name: apply_chromium_cdp_flag
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "apply_chromium_cdp_flag [--port N] [--network] [--fragment-path <path>] [--remove] [--dry-run]"
description: "Gestiona de forma idempotente el fragmento /etc/chromium.d/cdp que activa Chrome DevTools Protocol global en todo chromium que el usuario lance en el equipo. Escribe, actualiza o borra el fragmento con backup automático."
tags: [navegator, chromium, cdp, devtools, browser, automation, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
params:
- name: "--port N"
desc: "Puerto TCP donde Chromium escuchará conexiones CDP. Default 9222."
- name: "--network"
desc: "Si se pasa, añade --remote-debugging-address=0.0.0.0 (accesible desde la red local). Por defecto solo loopback (127.0.0.1). Imprime advertencia de seguridad."
- name: "--fragment-path <path>"
desc: "Ruta del fragmento a escribir/borrar. Default /etc/chromium.d/cdp."
- name: "--remove"
desc: "Borra el fragmento (desactiva CDP global). Idempotente: si no existe, no-op."
- name: "--dry-run"
desc: "Imprime el fragmento que se escribiría sin tocar nada. No requiere sudo."
output: "Sale 0 en éxito (aplicado, ya-aplicado, o eliminado). Sale != 0 en error con mensaje a stderr. En caso de actualización imprime ruta del backup creado."
file_path: "bash/functions/browser/apply_chromium_cdp_flag.sh"
---
## Ejemplo
```bash
# Activar CDP global en loopback puerto 9222 (proyecto web_scraping, regla 8)
source bash/functions/browser/apply_chromium_cdp_flag.sh
apply_chromium_cdp_flag
# Previsualizar el fragmento sin escribir nada (no requiere sudo)
apply_chromium_cdp_flag --port 9222 --dry-run
# Puerto alternativo (para correr en paralelo al navegador del usuario)
apply_chromium_cdp_flag --port 9333
# Activar expuesto a la red local (RIESGO: cualquier host de la LAN puede controlar el navegador)
apply_chromium_cdp_flag --port 9222 --network
# Desactivar CDP global
apply_chromium_cdp_flag --remove
# Ruta personalizada (útil en pruebas o entornos chroot)
apply_chromium_cdp_flag --port 9222 --fragment-path /tmp/test_cdp_fragment --dry-run
```
## Cuando usarla
Al preparar un PC nuevo para controlar el chromium diario del usuario vía CDP (primer setup del proyecto `web_scraping`, regla 8). Al cambiar el puerto CDP del sistema. Al desactivar esa capacidad antes de prestar o formatear el equipo. Sustituye el paso manual "crear `/etc/chromium.d/cdp` con sudo" documentado en `CHROMIUM_SYSTEM.md`.
## Gotchas
- **Requiere sudo** para escribir bajo `/etc/`. En este equipo usar `pass show claude/sudo | sudo -S apply_chromium_cdp_flag` o ejecutar como root.
- **`--network` (0.0.0.0) es un riesgo de seguridad serio**: cualquier máquina en la red local puede conectarse al puerto CDP y controlar Chromium completamente (leer cookies, sesiones, inyectar JavaScript). Solo usar en entornos de red aislados o laboratorios.
- **El chromium ya abierto antes del cambio no hereda el flag** hasta que se reinicie. El fragmento solo se aplica en el próximo arranque de `/usr/bin/chromium`.
- **Dos procesos chromium no pueden compartir el mismo puerto**. Si el usuario ya tiene un chromium con CDP en 9222, la automatización dedicada debe arrancar con `chrome_launch_go_browser` en otro puerto (ej. 9333) o usar `--port 9333` en esta función.
- **Idempotente**: si el fragmento ya existe con contenido idéntico, la función sale 0 sin tocar nada ni crear backup.
- **Backup automático**: al sobreescribir, crea `<path>.bak.YYYYMMDD`. Si ya existe un backup del mismo día, no lo sobreescribe (el primero del día se preserva).
- **Validación post-escritura**: tras escribir, verifica con `grep` que la línea `export CHROMIUM_FLAGS` con el puerto correcto quedó en el archivo. Si falla, restaura el backup y sale con error.
- Ver `projects/web_scraping/CHROMIUM_SYSTEM.md` para el contexto completo del sistema CDP de este equipo.
@@ -0,0 +1,205 @@
#!/usr/bin/env bash
# apply_chromium_cdp_flag — gestiona el fragmento /etc/chromium.d/cdp que activa CDP global.
#
# Uso:
# apply_chromium_cdp_flag [--port N] [--network] [--fragment-path <path>] [--remove] [--dry-run]
apply_chromium_cdp_flag() {
local port=9222
local network=0
local fragment_path="/etc/chromium.d/cdp"
local remove=0
local dry_run=0
# Parseo de argumentos
while [[ $# -gt 0 ]]; do
case "$1" in
--port)
port="$2"
shift 2
;;
--network)
network=1
shift
;;
--fragment-path)
fragment_path="$2"
shift 2
;;
--remove)
remove=1
shift
;;
--dry-run)
dry_run=1
shift
;;
*)
echo "apply_chromium_cdp_flag: argumento desconocido: $1" >&2
return 1
;;
esac
done
# Validación del puerto
if ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
echo "apply_chromium_cdp_flag: puerto inválido: $port (debe ser 1-65535)" >&2
return 1
fi
# Construcción del contenido del fragmento
local flags_line
if (( network )); then
echo "ADVERTENCIA DE SEGURIDAD: --network activa --remote-debugging-address=0.0.0.0." >&2
echo "El navegador quedará expuesto a toda la red local. Cualquier host en la red" >&2
echo "podrá controlar Chromium remotamente y leer todas las sesiones abiertas." >&2
flags_line='export CHROMIUM_FLAGS="$CHROMIUM_FLAGS --remote-debugging-port='"${port}"' --remote-allow-origins=* --remote-debugging-address=0.0.0.0"'
else
flags_line='export CHROMIUM_FLAGS="$CHROMIUM_FLAGS --remote-debugging-port='"${port}"' --remote-allow-origins=*"'
fi
local mode_label
if (( network )); then
mode_label="network (0.0.0.0)"
else
mode_label="loopback (127.0.0.1)"
fi
local fragment_content
fragment_content="# CDP global para automatizacion del navegador del usuario (proyecto web_scraping, regla 8).
# Bind ${mode_label} por defecto: el puerto solo
# es accesible desde 127.0.0.1, no desde la red.
${flags_line}"
# Modo --dry-run: solo mostrar y salir
if (( dry_run )); then
echo "--- dry-run: fragmento que se escribiría en ${fragment_path} ---"
echo "${fragment_content}"
echo "--- fin dry-run ---"
return 0
fi
# Modo --remove
if (( remove )); then
if [[ ! -e "$fragment_path" ]]; then
echo "apply_chromium_cdp_flag: ${fragment_path} no existe, nada que borrar."
return 0
fi
local backup_path="${fragment_path}.bak.$(date +%Y%m%d)"
if [[ ! -e "$backup_path" ]]; then
if [[ $EUID -eq 0 ]]; then
cp "$fragment_path" "$backup_path"
else
sudo cp "$fragment_path" "$backup_path" || {
echo "apply_chromium_cdp_flag: no se pudo crear backup en ${backup_path}" >&2
return 1
}
fi
fi
if [[ $EUID -eq 0 ]]; then
rm "$fragment_path"
else
sudo rm "$fragment_path" || {
echo "apply_chromium_cdp_flag: no se pudo borrar ${fragment_path}" >&2
return 1
}
fi
echo "apply_chromium_cdp_flag: fragmento eliminado (backup: ${backup_path})"
echo "Nota: el chromium ya abierto antes de este cambio no lo hereda hasta reiniciarlo."
return 0
fi
# Idempotencia: comparar con contenido actual
if [[ -f "$fragment_path" ]]; then
local current_content
current_content=$(cat "$fragment_path" 2>/dev/null)
if [[ "$current_content" == "$fragment_content" ]]; then
echo "apply_chromium_cdp_flag: ya aplicado, sin cambios (${fragment_path})."
return 0
fi
fi
# Crear directorio si falta
local fragment_dir
fragment_dir=$(dirname "$fragment_path")
if [[ ! -d "$fragment_dir" ]]; then
if [[ $EUID -eq 0 ]]; then
mkdir -p "$fragment_dir"
else
sudo mkdir -p "$fragment_dir" || {
echo "apply_chromium_cdp_flag: no se pudo crear ${fragment_dir}" >&2
return 1
}
fi
fi
# Backup si ya existe y difiere
if [[ -e "$fragment_path" ]]; then
local backup_path="${fragment_path}.bak.$(date +%Y%m%d)"
if [[ ! -e "$backup_path" ]]; then
if [[ $EUID -eq 0 ]]; then
cp "$fragment_path" "$backup_path"
else
sudo cp "$fragment_path" "$backup_path" || {
echo "apply_chromium_cdp_flag: no se pudo crear backup en ${backup_path}" >&2
return 1
}
fi
echo "apply_chromium_cdp_flag: backup creado en ${backup_path}"
fi
fi
# Escribir fragmento
local tmpfile
tmpfile=$(mktemp)
printf '%s\n' "$fragment_content" > "$tmpfile"
if [[ $EUID -eq 0 ]]; then
cp "$tmpfile" "$fragment_path"
chmod 644 "$fragment_path"
else
sudo cp "$tmpfile" "$fragment_path" || {
rm -f "$tmpfile"
echo "apply_chromium_cdp_flag: no se pudo escribir ${fragment_path}" >&2
return 1
}
sudo chmod 644 "$fragment_path" 2>/dev/null || true
fi
rm -f "$tmpfile"
# Validación post-escritura
local expected_line="--remote-debugging-port=${port}"
if ! grep -qF "$expected_line" "$fragment_path" 2>/dev/null; then
echo "apply_chromium_cdp_flag: validación fallida — la línea export no apareció en ${fragment_path}." >&2
# Restaurar backup si existe
local backup_path="${fragment_path}.bak.$(date +%Y%m%d)"
if [[ -f "$backup_path" ]]; then
if [[ $EUID -eq 0 ]]; then
cp "$backup_path" "$fragment_path"
else
sudo cp "$backup_path" "$fragment_path" 2>/dev/null || true
fi
echo "apply_chromium_cdp_flag: backup restaurado desde ${backup_path}" >&2
fi
return 1
fi
# Resumen final
echo "apply_chromium_cdp_flag: CDP global activado correctamente."
echo " Fragmento : ${fragment_path}"
echo " Puerto : ${port}"
echo " Modo : ${mode_label}"
echo ""
echo "Nota: el chromium ya abierto antes de este cambio no hereda el flag hasta reiniciarlo."
echo "Nota: dos procesos chromium no pueden compartir el mismo puerto; usa --port <otro> para"
echo " automatización dedicada que corra en paralelo al navegador del usuario."
}
# Auto-ejecución al correr el archivo directo (bash file.sh / fn run). Si se hace `source`,
# solo se define la función y no se ejecuta nada.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
apply_chromium_cdp_flag "$@"
fi
@@ -0,0 +1,66 @@
---
name: apply_chromium_extension_policy
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "apply_chromium_extension_policy --keep <ext_id> [--keep <ext_id>]... [--policy-path <path>] [--update-url <url>] [--dry-run]"
description: "Escribe de forma idempotente la política managed de Chromium (ExtensionInstallForcelist) que fuerza la instalación de un conjunto de extensiones en cualquier perfil del navegador. Crea backup con sufijo de fecha si el archivo preexiste y su contenido difiere. Requiere sudo para escribir en /etc; en modo --dry-run no toca el sistema."
tags: [chromium, extensions, policy, browser, navegator, managed-policy, idempotent]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--keep <ext_id>"
desc: "ID de extensión de Chrome Web Store a force-instalar (repetible, al menos uno obligatorio). Ejemplo: ddkjiahejlhfcafbddmgiahcphecmpfh (uBlock Origin Lite)."
- name: "--policy-path <path>"
desc: "Ruta del JSON de managed policy. Default: /etc/chromium/policies/managed/extensions.json."
- name: "--update-url <url>"
desc: "URL del servicio de actualización de extensiones. Default: https://clients2.google.com/service/update2/crx."
- name: "--dry-run"
desc: "Imprime el JSON que se escribiría sin tocar el sistema (no requiere sudo)."
output: "Escribe el JSON de política en policy-path y emite a stdout un resumen: extensiones aplicadas, ruta, backup creado y recordatorio de reinicio de Chromium. Sale 0 si la política se aplicó o ya estaba vigente. Sale != 0 en error."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/browser/apply_chromium_extension_policy.sh"
---
## Ejemplo
```bash
# Forzar solo uBlock Origin Lite en todos los perfiles (proyecto web_scraping, regla 9)
source bash/functions/browser/apply_chromium_extension_policy.sh
apply_chromium_extension_policy --keep ddkjiahejlhfcafbddmgiahcphecmpfh
# Previsualizar sin tocar el sistema (sin sudo)
apply_chromium_extension_policy --keep ddkjiahejlhfcafbddmgiahcphecmpfh --dry-run
# Forzar varias extensiones a la vez
apply_chromium_extension_policy \
--keep ddkjiahejlhfcafbddmgiahcphecmpfh \
--keep cjpalhdlnbpafiamejdnhcphjbkeiagm \
--policy-path /etc/chromium/policies/managed/extensions.json
# Usando pass para el sudo no interactivo (setup de este equipo)
# apply_chromium_extension_policy ya usa sudo tee internamente;
# si el entorno requiere pass: ejecutar como root o con SUDO_ASKPASS configurado.
```
## Cuando usarla
Al preparar un PC nuevo o cambiar qué extensiones de Chrome Web Store deben estar presentes en cualquier perfil de Chromium del equipo. Reemplaza el paso manual de editar el JSON de policy con sudo. Indispensable tras un fresh install o cuando se añade o retira una extensión del conjunto obligatorio del proyecto `web_scraping`.
## Gotchas
- **Requiere sudo** para escribir en `/etc/chromium/policies/managed/`. En este equipo se alimenta con `pass show claude/sudo | sudo -S <cmd>` para operaciones autónomas.
- **Chrome cachea la política en memoria**: hay que cerrar TODOS los procesos Chromium (`pkill -9 chromium`) y relanzar para que el cambio surta efecto en runtime. Alternativa sin cerrar: `chrome://policy` → botón "Reload policies".
- **Idempotente**: si el archivo de policy ya tiene el mismo contenido, la función detecta el no-op y sale 0 sin escribir nada.
- **Backup por día**: si el contenido difiere, crea `<policy-path>.bak.YYYYMMDD`. Si el backup del día ya existe, no lo sobreescribe. Los backups no se limpian automáticamente.
- **`ExtensionInstallForcelist` solo gestiona las extensiones listadas** — no elimina extensiones que el usuario haya instalado manualmente; solo garantiza que las listadas estén instaladas.
- **No aplica a Chrome stable 138+**: en Chrome estable la flag `--load-extension` está desactivada, pero las managed policies siguen siendo el mecanismo correcto para force-install desde Web Store.
- Para referencia del sistema completo: `projects/web_scraping/CHROMIUM_SYSTEM.md`.
@@ -0,0 +1,205 @@
#!/usr/bin/env bash
# apply_chromium_extension_policy — Escribe de forma idempotente la política managed de Chromium
# que fuerza la instalación de un conjunto de extensiones en CUALQUIER perfil del navegador.
apply_chromium_extension_policy() {
local policy_path="/etc/chromium/policies/managed/extensions.json"
local update_url="https://clients2.google.com/service/update2/crx"
local dry_run=0
local -a ext_ids=()
# --- Parseo de argumentos ---
while [[ $# -gt 0 ]]; do
case "$1" in
--keep)
if [[ -z "${2:-}" ]]; then
echo "apply_chromium_extension_policy: --keep requiere un ID de extensión" >&2
return 1
fi
ext_ids+=("$2")
shift 2
;;
--policy-path)
if [[ -z "${2:-}" ]]; then
echo "apply_chromium_extension_policy: --policy-path requiere un valor" >&2
return 1
fi
policy_path="$2"
shift 2
;;
--update-url)
if [[ -z "${2:-}" ]]; then
echo "apply_chromium_extension_policy: --update-url requiere un valor" >&2
return 1
fi
update_url="$2"
shift 2
;;
--dry-run)
dry_run=1
shift
;;
*)
echo "apply_chromium_extension_policy: argumento desconocido: $1" >&2
echo "Uso: apply_chromium_extension_policy --keep <ext_id> [--keep <ext_id>]... [--policy-path <path>] [--update-url <url>] [--dry-run]" >&2
return 1
;;
esac
done
# --- Validar que hay al menos una extensión ---
if [[ ${#ext_ids[@]} -eq 0 ]]; then
echo "apply_chromium_extension_policy: se requiere al menos un --keep <ext_id>" >&2
return 1
fi
# --- Construir el JSON ---
local forcelist_entries=""
local first=1
for id in "${ext_ids[@]}"; do
if [[ $first -eq 0 ]]; then
forcelist_entries+=","$'\n'
fi
forcelist_entries+=" \"${id};${update_url}\""
first=0
done
local new_json
new_json=$(printf '{\n "ExtensionInstallForcelist": [\n%s\n ]\n}\n' "$forcelist_entries")
# --- Modo dry-run ---
if [[ $dry_run -eq 1 ]]; then
echo "[dry-run] JSON que se escribiría en: ${policy_path}"
echo "---"
echo "$new_json"
echo "---"
echo "[dry-run] No se ha modificado el sistema."
return 0
fi
# --- Idempotencia: comparar con contenido actual ---
if [[ -f "$policy_path" ]]; then
local current_content
current_content=$(cat "$policy_path" 2>/dev/null || true)
if [[ "$current_content" == "$new_json" ]]; then
echo "apply_chromium_extension_policy: política ya aplicada (sin cambios). Nada que hacer."
return 0
fi
fi
# --- Backup del archivo existente ---
local backup_path=""
if [[ -f "$policy_path" ]]; then
local date_suffix
date_suffix=$(date +%Y%m%d)
backup_path="${policy_path}.bak.${date_suffix}"
if [[ ! -f "$backup_path" ]]; then
echo "apply_chromium_extension_policy: creando backup → ${backup_path}"
if [[ $EUID -ne 0 ]]; then
sudo cp "$policy_path" "$backup_path" || {
echo "apply_chromium_extension_policy: no se pudo crear el backup en ${backup_path}" >&2
return 1
}
else
cp "$policy_path" "$backup_path" || {
echo "apply_chromium_extension_policy: no se pudo crear el backup en ${backup_path}" >&2
return 1
}
fi
else
echo "apply_chromium_extension_policy: backup del día ya existe (${backup_path}), se omite."
fi
fi
# --- Crear directorio padre si no existe ---
local policy_dir
policy_dir=$(dirname "$policy_path")
if [[ ! -d "$policy_dir" ]]; then
echo "apply_chromium_extension_policy: creando directorio ${policy_dir}"
if [[ $EUID -ne 0 ]]; then
sudo mkdir -p "$policy_dir" || {
echo "apply_chromium_extension_policy: no se pudo crear el directorio ${policy_dir}" >&2
return 1
}
else
mkdir -p "$policy_dir" || {
echo "apply_chromium_extension_policy: no se pudo crear el directorio ${policy_dir}" >&2
return 1
}
fi
fi
# --- Escribir el JSON vía tmpfile + sudo cp / tee ---
local tmpfile
tmpfile=$(mktemp /tmp/chromium_policy_XXXXXX.json)
echo "$new_json" > "$tmpfile"
if [[ $EUID -ne 0 ]]; then
sudo cp "$tmpfile" "$policy_path" || {
echo "apply_chromium_extension_policy: no se pudo escribir ${policy_path}" >&2
rm -f "$tmpfile"
# Restaurar backup si hubo
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
echo "apply_chromium_extension_policy: restaurando backup tras error..."
sudo cp "$backup_path" "$policy_path" 2>/dev/null || true
fi
return 1
}
else
cp "$tmpfile" "$policy_path" || {
echo "apply_chromium_extension_policy: no se pudo escribir ${policy_path}" >&2
rm -f "$tmpfile"
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
echo "apply_chromium_extension_policy: restaurando backup tras error..."
cp "$backup_path" "$policy_path" 2>/dev/null || true
fi
return 1
}
fi
rm -f "$tmpfile"
# --- Validar el JSON escrito ---
local validation_ok=0
if command -v python3 &>/dev/null; then
python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$policy_path" 2>/dev/null && validation_ok=1
elif command -v jq &>/dev/null; then
jq . "$policy_path" &>/dev/null && validation_ok=1
else
# Sin parser disponible, asumir OK (el JSON lo generamos nosotros)
validation_ok=1
fi
if [[ $validation_ok -eq 0 ]]; then
echo "apply_chromium_extension_policy: el JSON escrito no es válido — restaurando backup" >&2
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
if [[ $EUID -ne 0 ]]; then
sudo cp "$backup_path" "$policy_path" 2>/dev/null || true
else
cp "$backup_path" "$policy_path" 2>/dev/null || true
fi
fi
return 1
fi
# --- Resumen final ---
echo "apply_chromium_extension_policy: política aplicada correctamente."
echo " Ruta : ${policy_path}"
echo " Extensiones forzadas (${#ext_ids[@]}):"
for id in "${ext_ids[@]}"; do
echo " - ${id}"
done
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
echo " Backup : ${backup_path}"
fi
echo ""
echo " AVISO: Chromium cachea la politica en memoria. Para que surta efecto:"
echo " pkill -9 chromium (cierra TODOS los procesos)"
echo " # Luego relanza Chromium. O desde un Chromium abierto:"
echo " # chrome://policy → 'Reload policies'"
}
# Auto-ejecución al correr el archivo directo (bash file.sh / fn run). Si se hace `source`,
# solo se define la función y no se ejecuta nada.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
apply_chromium_extension_policy "$@"
fi