Skip to content

feat(secrets): smoke de migración legacy aislado + README evidencia (#106 AC2) - #203

Merged
egdev6 merged 13 commits into
egdev6:mainfrom
sosan:feat/secrets-migrate-smoke
Nov 6, 2026
Merged

egdev6 merged 13 commits into
egdev6:mainfrom
sosan:feat/secrets-migrate-smoke

Conversation

@sosan

@sosan sosan commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Qué entrega

Ejercita la migración del servicio legacy (criterio AC2 del issue #106) en Secret Service real del CI, respondiendo a la revisión del owner (2026-10-28):

«Confina el smoke a un llavero desechable con aislamiento verificable del bus y del almacenamiento, y sustituye la indicación de ejecutar los tests ignorados directamente en el escritorio por un procedimiento aislado».

Cambios (3 archivos, confinados)

  • src-tauri/src/secrets.rs: smoke #[ignore] linux_legacy_migrate_once_then_legacy_is_gone:
    • Puebla LEGACY_SERVICE (es.egdev.ai-usage) directamente con una clave base de la allowlist (nan_session, sin @; las claves con @ no migran por diseño de get()).
    • get(), con el destino actual vacío, recupera el valor mediante migración (una lectura); el valor queda reescrito en el destino actual (es.egdev.quorb) y el legado devuelve NoEntry.
    • Aislamiento obligatorio: exige QUORB_SECRETS_SMOKE_ISOLATED=1 (exportada solo dentro del dbus-run-session y el gnome-keyring efímero del workflow) y falla antes de tocar nada si no está presente. CleanupGuard se crea antes de cualquier lectura y su Drop vuelve a verificar la variable antes de borrar. La precondición distingue NoEntry de un error de lectura.
    • Se actualizan los comentarios de documentación de ambos smokes: la única vía soportada es el CI aislado, no un escritorio.
  • .github/workflows/linux-ci.yml: el paso «Secret Service smoke» exporta la variable de aislamiento y ejecuta ambos smokes (round trip y migración).
  • docs/linux-l1-evidence/README.md: la migración pasa de «pendiente» a «ejercitada» (run 40651008234) y se documenta la ejecución segura.

Revisión

El cambio fue revisado por un agente rust-reviewer: sin CRITICAL ni HIGH. Se detectaron 1 MEDIUM y 2 LOW, todos corregidos en 9a9764d (reverificación del aislamiento en el Drop del guard, comprobación del aislamiento también en el round trip y documentación del valor sin registrarlo en los logs durante el camino feliz).

Nota

Los cambios no entraron en #176 (ya fusionada); esta PR los aporta sobre main actual (0.8.0), con el diff confinado a los tres archivos.

Resuelve el punto AC2 del issue #106.

sosan added a commit to sosan/quorb that referenced this pull request Oct 28, 2026
…6#203)

El check de CI (Windows) detectaba drift solo en los header tree hashes:
los textos coinciden, cambió la estructura del árbol tras el merge con el
main 0.8.0. Regenerado con node scripts/i18n-inventory.mjs --out.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@egdev6

egdev6 commented Oct 29, 2026

Copy link
Copy Markdown
Owner

@sosan gracias por retomarlo tan rápido. He revisado el diff completo contra main: la precondición que distingue NoEntry de un error de lectura es justo lo que faltaba, y usar una clave base sin @ es correcto. Aun así necesito tres cambios antes de integrarlo.

1. Separar el laboratorio WSL

El cuerpo dice «3 archivos, confinados», pero la PR trae 5: incluye .github/workflows/w3-evidence.yml (501 líneas, el laboratorio de #90) y arrastra unos 27 commits de ese laboratorio desde el main del fork. Añadir un workflow a main es otro alcance: por favor, sácalo de esta PR (por ejemplo, rama nueva desde egdev6/main con solo los commits del smoke) y, si quieres integrarlo, propónlo en su propia PR enlazada a #90.

2. El smoke todavía puede borrar un secreto real

  • El CleanupGuard se crea antes de comprobar que el destino está vacío. Si existe un nan_session real, el assert falla y el Drop borra ese secreto y el legado. Es el caso que señalé en feat(linux): adaptar sesiones, secretos y login CLI nativos #106: mover el guard no basta.
  • La re-verificación de QUORB_SECRETS_SMOKE_ISOLATED en el Drop no protege si la variable está exportada.
  • La precondición no mira el servicio legado: si tenía un valor real, set_password lo sobrescribe.
  • La variable declara el aislamiento, no lo verifica.

Propuesta:

  • Comprobar que el destino actual y el legado están en NoEntry antes de crear el guard, y limpiar solo lo que escribió el test.
  • En el workflow, arrancar gnome-keyring con HOME/XDG_DATA_HOME en un directorio temporal, y que el test compruebe que está en ese entorno (por ejemplo, que XDG_DATA_HOME cuelga del directorio temporal que le pasa el workflow) en lugar de fiarse solo de la variable.

3. Ejecutar el código nuevo

linux-ci.yml solo se lanza a mano y su última ejecución en tu fork es la 40651008234, sobre 0c75fab: el código anterior, sin la variable ni el guard reordenado. Los checks de esta PR no ejecutan los tests #[ignore]. Lanza linux-ci.yml sobre el head de la PR y enlaza esa ejecución en el README y aquí, en lugar de la anterior.

Detalle menor: rustfmt dejó algunos comentarios mal alineados junto a los // gitleaks:allow; si los reubicas al hacer los cambios, se lee mejor.

sosan and others added 3 commits October 31, 2026 00:55
…v6#106, AC2)

Ejercita migrate_legacy() en Secret Service real del CI (run
40651008234) según la revisión del owner (2026-10-28):

- smoke #[ignore] linux_legacy_migrate_once_then_legacy_is_gone: puebla
  LEGACY_SERVICE (es.egdev.ai-usage) con una clave base (nan_session, sin
  @: las claves con @ no migran por diseño), get() la recupera por
  migración, queda reescrita en el destino actual (es.egdev.quorb) y el
  legado devuelve NoEntry. Sin imprimir valores.
- AISLAMIENTO OBLIGATORIO: el smoke exige QUORB_SECRETS_SMOKE_ISOLATED=1
  (definida solo dentro del dbus-run-session + gnome-keyring efímero del
  workflow) y falla antes de tocar nada sin ella — nunca un escritorio
  con llavero real. CleanupGuard creado antes de cualquier lectura y con
  distinción NoEntry vs error de lectura en la precondición.
- linux-ci.yml: el paso "Secret Service smoke" exporta la var de
  aislamiento y corre ambos smokes.
- README de evidencia: la migración pasa de pendiente a ejercitada, con
  el run enlazado y la ejecución segura documentada (solo vía CI aislado,
  no manual en escritorio).

Co-Authored-By: Claude Code <noreply@anthropic.com>
…dev6#106)

Revisión del agente rust-reviewer (2026-10-28), sin CRITICAL/HIGH:

- MEDIUM: el Drop del CleanupGuard re-verifica QUORB_SECRETS_SMOKE_ISOLATED
  antes de borrar; si la precondición de aislamiento falló (entorno con
  llavero real donde la var se filtró y el destino NO estaba vacío), no
  borra nada. El comentario deja claro que la protección real es el
  keyring efímero del CI, no el assert.
- LOW: el round trip (linux_secret_service_round_trip) ahora exige el
  mismo QUORB_SECRETS_SMOKE_ISOLATED=1 antes de tocar nada (coherencia
  doc/código).
- LOW: doc del smoke de migración ajustado — el valor no se loguea en el
  camino feliz (aparece solo en un assert_eq! si algo falla, y es una
  constante de prueba no secreta).

Co-Authored-By: Claude Code <noreply@anthropic.com>
El inventario no se edita a mano: se regenera. Su header declara el
hash del árbol `src-tauri/src/`, que cambia con el smoke de migracion
de secrets. Regenerado y verificado con
`node scripts/i18n-inventory.mjs --check`.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@sosan
sosan force-pushed the feat/secrets-migrate-smoke branch from d9be5b5 to 06131e6 Compare October 31, 2026 00:47
@egdev6

egdev6 commented Oct 31, 2026

Copy link
Copy Markdown
Owner

@sosan, revisé el head 06131e669520f6b077c8d55bca12e6de4c021dc0. El laboratorio WSL ya salió del diff, gracias por separar ese alcance. Quedan cuatro archivos, incluido el hash generado del inventario i18n. Antes de integrar siguen pendientes los otros dos pedidos de la revisión anterior:

  1. Seguridad del smoke y aislamiento. En src-tauri/src/secrets.rs:389 el CleanupGuard todavía se crea antes de comprobar el destino. Si existe un secreto y falla el assert, el Drop borra destino y legado porque la variable de aislamiento sigue en 1. Además, el legado no se comprueba antes de sobrescribirlo. Ambos servicios deben devolver NoEntry antes de crear el guard, y la limpieza debe limitarse a lo que escribió el test. En el workflow, dbus-run-session aísla el bus, pero siguen faltando HOME/XDG_DATA_HOME temporales y una comprobación del entorno desechable: la variable declara aislamiento, no lo verifica. Ajustemos también el README, que hoy garantiza una protección que el código no demuestra.

  2. Evidencia del código actualizado. El README sigue enlazando el run 40651008234, sobre 0c75fab; es también la última ejecución de linux-ci.yml que encontré en el fork, no corresponde al head actual. Los checks de esta PR están verdes, pero no acreditan la ejecución de estos tests #[ignore]. Una vez corregido el aislamiento, ejecuta el workflow sobre el head corregido y enlaza esa ejecución tanto aquí como en el README.

No ejecuté los smokes localmente para no tocar un llavero real. Mantendría la PR sin integrar hasta resolver ambos puntos.

sosan and others added 4 commits November 4, 2026 18:32
…gdev6#203)

Revisión del mantenedor (2026-10-31):

- El CleanupGuard se creaba ANTES de comprobar el destino: si existía un
  secreto y fallaba el assert, el Drop borraba destino y legado. Ahora el
  smoke comprueba primero que el destino actual Y el legado devuelven
  NoEntry; sin secreto real no hay nada que borrar, y hasta que esa
  precondición pasa no existe guard.
- La limpieza se acota a lo que ESCRIBE el test: flags de escritura para el
  destino y para el legado, activados justo antes de cada escritura.
- El aislamiento se VERIFICA, no se declara: el workflow exporta HOME y
  XDG_DATA_HOME temporales bajo QUORB_SECRETS_SMOKE_TMPHOME y el smoke
  comprueba que cuelgan de ese directorio desechable antes de tocar nada.
- README: documenta el orden nuevo y deja de atribuir al run antiguo la
  evidencia del head actual.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ev6#203)

Exportar HOME temporal rompía rustup ("no default is configured"):
cargo/rustup buscan su toolchain en el HOME. CARGO_HOME y RUSTUP_HOME se
mantienen en el HOME original del runner mientras el keyring y el smoke
usan el directorio desechable.

Co-Authored-By: Claude Code <noreply@anthropic.com>
egdev6#203)

Ejecución de linux-ci.yml sobre el head corregido (run 41885715837):
round trip y migración legacy con HOME/XDG_DATA_HOME temporales verificados.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@sosan

sosan commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Gracias de nuevo por la revisión, @egdev6. Los dos puntos quedan resueltos sobre el head e2057bc:

1. Seguridad del smoke y aislamiento.

  • Precondición antes del guard: el smoke comprueba primero que el destino actual (nan_session) y el legado (es.egdev.ai-usage) devuelven NoEntry. Si alguno tuviera un secreto real, falla AQUÍ, sin haber creado el CleanupGuard — no existe ninguna limpieza que pueda borrar nada.
  • Limpieza acotada a lo que escribió el test: el guard lleva flags (current_written, legacy_written) que se activan justo antes de cada escritura; el Drop solo borra las entradas que el test creó (el legado y, tras la migración, el destino). Nunca toca una entrada preexistente.
  • Aislamiento verificado, no declarado: el workflow exporta HOME y XDG_DATA_HOME temporales bajo QUORB_SECRETS_SMOKE_TMPHOME (el keyring del runner vive ahí, no en el HOME real) y el smoke comprueba que HOME == TMPHOME y que XDG_DATA_HOME cuelga de ese directorio antes de tocar nada. Sin la variable, o con las rutas fuera del temporal, el smoke falla antes de tocar ninguna clave. (Nota: CARGO_HOME/RUSTUP_HOME se preservan del HOME original para no romper rustup.)
  • README actualizado: documenta el orden nuevo y deja de atribuir al run antiguo una protección que el código no demostraba.

2. Evidencia del código actualizado.

linux-ci.yml ejecutado sobre el head corregido (55d3d75): run 41885715837 — success, con ambos smokes en el entorno aislado verificado:

  • linux_secret_service_round_trip ... ok
  • linux_legacy_migrate_once_then_legacy_is_gone ... ok
  • QUORB_SECRETS_SMOKE_ISOLATED=1 (aislamiento verificado) con TMPHOME/HOME/XDG_DATA_HOME temporales en el log.

El README de evidencias enlaza esta ejecución (la anterior, 40651008234 sobre 0c75fab, queda marcada como código previo, sin acreditar el head actual).

Queda listo para la revisión de integración.

@egdev6

egdev6 commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Gracias, @sosan. Revisé el head e2057bc y los pedidos anteriores están resueltos: la precondición de NoEntry en el destino y en el legado ocurre antes de crear el guard, la limpieza se limita a lo que escribió el test, el keyring arranca con HOME/XDG_DATA_HOME temporales y el run 41885715837 sobre 55d3d75 acredita el head (después solo cambia una línea del README). Ya no veo riesgo de borrar un secreto real.

Antes de integrar, te pido estos ajustes menores:

  1. El aislamiento verificado no cubre el bus. El smoke compara HOME y XDG_DATA_HOME con QUORB_SECRETS_SMOKE_TMPHOME, pero Secret Service se alcanza por DBUS_SESSION_BUS_ADDRESS. Con esas variables exportadas en un escritorio, escribiría en el llavero real. La seguridad hoy la dan la precondición y la limpieza acotada, que son correctas. Propongo una de dos: verificar también que el bus es el de la sesión efímera (por ejemplo, que el workflow exporte la dirección esperada y el smoke la compare), o ajustar el README para no afirmar que el aislamiento está «verificado» y explicar que la protección real es la precondición.
  2. El round trip no comprueba HOME/XDG_DATA_HOME. Solo exige QUORB_SECRETS_SMOKE_ISOLATED=1. Usa una clave única, así que el riesgo es bajo, pero conviene extraer la verificación a un helper compartido para que ambos smokes apliquen la misma regla.
  3. El README se contradice. Dice que «CleanupGuard limpia ambos servicios al terminar» y, más abajo, que solo borra lo que escribió el test. Basta con dejar la segunda formulación.
  4. Borde de la precondición. Se acepta Ok(v) con valor vacío como si no existiera, y current_written se activa antes de get(), así que el Drop borraría una entrada vacía preexistente que el test no creó. Lo más simple es exigir NoEntry estricto en ambos servicios.

Con esto queda lista para integrar; la haré con squash por el merge commit de la historia.

Revisión estática sobre el diff y el run de CI; no ejecuté los smokes.

sosan and others added 5 commits November 5, 2026 23:50
…te (egdev6#203)

Cuatro ajustes de la revisión del mantenedor (2026-11-05):

1. El aislamiento verificado ahora cubre el BUS: el workflow exporta la
   dirección de la sesión efímera (QUORB_SECRETS_SMOKE_DBUS_ADDR) y el
   smoke la compara con la que el proceso usa — Secret Service se alcanza
   por DBUS_SESSION_BUS_ADDRESS, no por el HOME.
2. Ambos smokes (round trip y migración) comparten el mismo helper
   `assert_smoke_isolation()` (variable + HOME/XDG_DATA_HOME + bus).
3. README: se elimina la formulación «CleanupGuard limpia ambos servicios»;
   queda solo la correcta («solo borra lo que el test escribió»).
4. Precondición NoEntry ESTRICTO: una entrada existente aunque vacía aborta
   el smoke (el Drop no borra una entrada que el test no creó); el flag
   current_written se activa tras comprobar que la migración escribió.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review rust (2026-11-05):

- CRITICAL: `assert_smoke_isolation` quedó definida dos veces (E0428): el
  crate de tests no compilaba y ningún smoke llegaba a ejecutarse. Se
  conserva una sola definición.
- MEDIUM: los comentarios del guard y del README ya no dicen «justo antes
  de cada escritura»: el flag del destino se activa tras verificar la
  migración (el del legado, antes de su set_password).
- LOW: el bus no puede estar vacío (cierra el caso de dos valores vacíos
  que pasaban el assert_eq) y el doc no asume `abstract=`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…icto (egdev6#203)

Run 42128707783 sobre 98061b8: round trip y migración en verde en el
entorno aislado (HOME/XDG_DATA_HOME/bus temporales).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sosan

sosan commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Los cuatro ajustes quedan aplicados sobre el head 4d44208, con el smoke real en verde:

1. El aislamiento verificado ahora cubre el bus (opción 1). El workflow exporta la dirección de la sesión efímera (QUORB_SECRETS_SMOKE_DBUS_ADDR="$DBUS_SESSION_BUS_ADDRESS" dentro de dbus-run-session) y el smoke la compara con la que el proceso usa. Secret Service se alcanza por el bus, no por el HOME, así que un escritorio con las variables exportadas pero su bus propio no pasa esta puerta.

2. Helper compartido. assert_smoke_isolation() centraliza la regla (variable + HOME/XDG_DATA_HOME temporales + bus) y ambos smokes la llaman antes de tocar nada; el round trip ya no se queda solo con la variable.

3. README coherente. Se eliminó la frase «CleanupGuard limpia ambos servicios al terminar»; queda solo la formulación de limpieza acotada. El bloque de aislamiento documenta también el bus y la regla compartida.

4. NoEntry ESTRICTO. Una entrada preexistente —aunque esté vacía— aborta el smoke antes de crear el guard, y current_written se activa después de comprobar que la migración escribió (el flag del legado sigue antes de su set_password, porque esa escritura sí crea la entrada). El Drop no puede borrar nada que el test no haya creado.

Review de la skill (adicional), ya absorbida: detectó un CRITICAL mío — el helper había quedado duplicado (E0428: el crate de tests no compilaba). Corregido a una sola definición; además se cierra el caso de bus vacío (assert!(!actual.is_empty())) y se reformularon los comentarios/README que aún decían «justo antes de cada escritura» (el flag del destino es ahora tras verificar la migración).

Evidencia del código actualizado (el mantenedor pidió ejecutar el workflow sobre el head corregido): run 42128707783 sobre 98061b8 — success, ambos smokes ok con el aislamiento verificado:

  • linux_secret_service_round_trip ... ok
  • linux_legacy_migrate_once_then_legacy_is_gone ... ok
  • QUORB_SECRETS_SMOKE_ISOLATED=1 (aislamiento verificado) con TMPHOME/HOME/XDG_DATA_HOME temporales y QUORB_SECRETS_SMOKE_DBUS_ADDR en el log.

El README enlaza esta ejecución (la anterior queda como código previo). Lista para el squash.

@egdev6
egdev6 merged commit dbd395c into egdev6:main Nov 6, 2026
5 checks passed
@egdev6

egdev6 commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Gracias, @sosan. Revisé el head 35cd0e0 y los cuatro ajustes están aplicados: la verificación del bus, el helper compartido assert_smoke_isolation() en ambos smokes, el README coherente con la limpieza acotada y NoEntry estricto con current_written activado después de la migración.

El run 42128707783 sobre 98061b8 acredita el código que se integra: desde entonces solo cambia la línea del enlace en el README y el merge de main.

Queda aprobada; la integro con squash. Con esto, el criterio AC2 de #106 queda ejercitado en un Secret Service real y aislado. Gracias por la paciencia a lo largo de las rondas de revisión: el smoke quedó mucho más seguro que al principio.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants