Published

HANDOFF · 2026-08-15 — el sujeto humano, el plan que manda, y la cara medida

Connect any source, model it as an ontology, transform it, and operationalize it, analytics, automation and machine learning, under one governed, self-hostable roof. --- Most teams stitch the...

HANDOFF · 2026-08-15 — el sujeto humano, el plan que manda, y la cara medida

⛔ SUPERSEDED por handoff-2026-08-15b-gobernanza-en-sql.md

Este documento se conserva como la foto de su momento. Cinco de sus afirmaciones son
hoy falsas
, y se listan aquí para que nadie las herede leyendo sólo esta página:

§lo que dicelo que se midió después
3·2«Ninguna API de gobernanza»Falso ya entonces: /api/management/v1 (C4) existía completa —8 recursos, authz, auditoría—, sólo que inerte tras ENABLE_INDEX_MANAGEMENT_API. Hoy además hay /api/governance, autenticada por el token del realm
3·3«No existe OWNERSHIP»🏁 Existe, es exclusivo por índice, y decideGrantAuthority responde «quién puede conceder»
3·1«conceder hoy escalaría privilegios»🏁 Resuelto por el catalog_role dedicado, con gate de control negativo
3·2«0 grants de objeto»Sigue siendo cierto en número, pero ya hay camino: API, SQL y UI
1«F·1/F·2 · federación 🏁»⚠️ La fontanería lo estaba; el dato no: ninguna persona llevaba clerk_user_id, así que el puente estaba montado y vacío

Y un número suyo salió de un instrumento roto: los «109 del owner + 18 de servicios» de
datasets.created_by venían de una sonda que clasificaba por forma UUID, cuando los
sujetos de Clerk son user_… (texto). El reparto real está en g5-0-fuente-del-dueno.ts.

Lo que de este documento SIGUE VALIENDO: las trampas del §5, el orden acordado del §4
(que se ha completado entero) y las decisiones tomadas, que no han cambiado.

Para qué es esto. La foto autosuficiente para retomar. La sesión cerró el frente del
sujeto (Keycloak), asentó el paradigma nuevo del parser, y midió por primera vez la
superficie
. Lo que queda tiene orden acordado y está al final.

⚠️ SUPERSEDE a handoff-2026-08-14-plan-gobernado.md.
Aquél sigue valiendo para el detalle de S·0-S·5; para el estado, manda éste.

Regla de la casa: cada hecho lleva el comando que lo demuestra.


0 · Lo que hay que saber en diez líneas

  1. 🏁 Existe el SUJETO HUMANO. Hasta hoy el catálogo sólo hablaba con servicios: el realm tenía 16 clients, un usuario que nunca pidió token y cero grupos.
  2. 🏁 S·4 quedó CERRADO EN VIVO — la deuda que decía «no se puede afirmar que la puerta esté cerrada contra un tercero».
  3. 🏁 Keycloak federado con Clerk, y el gobierno le aplica a una persona (F·2 verde).
  4. 🏁 El plan dejó de ser suplente: el destino de escritura lo da el plan y sólo el plan.
  5. 🏁 La superficie está medida: 42 pantallas · 27 vivas · 6 maquetas · 0 huérfanas (con trinquete).
  6. Conceder hoy escalaría privilegios en silencio — el hallazgo más peligroso, §3·1.
  7. No hay API de gobernanza, ni roles de negocio (0), ni grants de objeto (0).
  8. ⚠️ Producción sirve 17178a5; hay 3 commits posteriores sin desplegar (docs y superficie). Ninguno cambia comportamiento de datos.
  9. ⚠️ El cluster y Spark siguen encendidos desde el 08-14, y eso cuesta.
  10. ⚠️ El usuario de QA carbon-qa-humano quedó en el realm con su contraseña en el historial de la sesión. Rotarlo o borrarlo.

1 · Lo cerrado, con su gate

gate
🏁 PParticionar por la columna de política — el techo de S·3 es un requisito comprobablep0·p0b·p1·p2-censo-servibilidad.ts
🏁 S·5·①②El reintento OCC era heredado; las 7 propiedades de robustez, declaradass5-gate-reintento-occ.ts · s5d (104 tablas) · s5e
🏁 K·1La cara deja de suponer que todo token es un servicio16 tests · k2-gate-sujeto-humano.ts
🏁 K·3S·4 contra un tercero, en vivok3-gate-s4-en-vivo.ts
🏁 F·1/F·2Federación Keycloak ← Clerk, y el gobierno aplica a una personaf2-gate-federacion.ts
🏁 P·4El plan deja de ser suplente en el destino de escrituraverificado en vivo con ALTER TABLE … ADD COLUMN
🏁 SUP·0/1La superficie medida, y las huérfanas cerradasnpm run check:superficie

364 tests en lib/governance · tsc limpio salvo lo de access-control-policies/, que es trabajo en curso del owner.


2 · ⭐ Los hallazgos que ordenan lo que queda

  1. ⭐⭐⭐ Podar no es lo que protege. Sin partición se poda igual y queda residuo; bucket también. Sólo identity resuelve el filtro. Y denegar el residual está alineado con el estándar: el issue de Iceberg #10909 —que el catálogo mande expresiones para que el motor las aplique— se cerró como not planned. El catálogo devuelve lo autorizado; no delega la aplicación.
  2. ⭐⭐⭐ El reintento OCC ya existía, heredado del cliente Iceberg. Se estuvo a punto de construir un bucle al lado de uno que funciona.
  3. ⭐⭐ Un paradigma que sólo actúa cuando el otro falla no está asentado: está de guardia. El destino salía siempre de la regex y el plan sólo entraba si ésta fallaba.
  4. ⭐⭐ Nuestro RBAC es Polaris literal (privilegio → catalog_role → principal_role → principal, con muchos-a-muchos en medio), y la política en el plan es lo que Databricks tiene en beta. En sustrato estamos en el estándar.
  5. ⭐⭐ Lo que falta no es arquitectura: es superficie.

3 · ⛔ Lo que está ABIERTO, por orden de daño

3·1 · ⛔⛔ Conceder hoy escalaría privilegios, en silencio

Medido (g3-acoplamiento-de-roles.ts): sólo 2 de 7 catalog_roles son exclusivos.

ws-editor  → arrastra a  query-engines, sql-editor, ws-viewer
ws-viewer  → arrastra a  query-engines, sql-editor, ws-editor

Un GRANT MODIFY TO ws-editor escrito en el catalog_role compartido se lo daría también a ws-viewer. ⚠️ No es un defecto del modelo de Polaris —la relación es muchos-a-muchos por diseño—: es que hay que crear catalog_roles con propósito, no reutilizar los compartidos. ⇒ El aplicador necesita rol dedicado antes de la primera concesión real.

3·2 · ⛔ La superficie de gobernanza no tiene con quién hablar

  • Ninguna API de gobernanza: ni grants, ni privilegios, ni políticas, ni tags. Lo único es /api/organization/roles, que es Clerk.
  • Tres vocabularios de «rol» que no se hablan: los de Clerk · principal_roles del catálogo (ws-admin, sql-editor…) · los inventados de la UI (data_steward, viewer).
  • 0 roles de negocio y 0 grants de objeto (224 workspace + 24 schema).

3·3 · ⛔ No existe OWNERSHIP — y es la misma pieza que «quién puede conceder»

datasets.created_by significa «quién lo tecleó» (109 del owner + 18 de servicios), no dueño. Sin dueño no hay respuesta a «¿quién tiene derecho a hacer este GRANT?», y esa pregunta bloquea la API que la UI ya pide.

3·4 · Los demás, ya conocidos

La máscara de columna no tiene camino: en el plan no se puede (medido) ⇒ esa tabla no se sirve
La Action atómica es imposible: /transactions/commit no existe en Gravitino (verificado contra el servidor)
S·5·③: que un reintento tras un 500 no duplique — sin gate
⚠️El bridge comparte UNA sesión de Spark Connect: un fallo suyo se llevó 5 escrituras en el mismo milisegundo
⚠️INSERT … VALUES vetado con motivo caducado; INSERT … SELECT no (su fuente no se cualifica)
⚠️PLAN_AUDITOR sigue en shadow: le falta ③ del criterio (12 formas de usuario, hoy 0) y no se puede cerrar sin tráfico real — hacerlo con un arnés sería la trampa que el propio módulo describe

4 · ⏭️ EL ORDEN ACORDADO — por aquí sigue la próxima sesión

①  OWNERSHIP                     ← desbloquea «quién puede conceder»; es G·5 y SUP·3 a la vez
②  Roles en KEYCLOAK, con composites   ← la jerarquía de Snowflake, que Clerk no sabe modelar
③  El aplicador con catalog_role DEDICADO   ← antes de la primera concesión real (§3·1)
④  La API de grants POR OBJETO   ← lo que la UI de Roles & Privileges ya pide
⑤  USE SCHEMA / MANAGE GRANTS    ← el cerrojo, cuando ya haya puerta

Las decisiones ya tomadas, para no repetirlas

  • A ROLES, no a personas. Un email o un user_… en un GRANT se rechazan con un motivo que dice qué hacer.
  • Keycloak = fuente de la MEMBRESÍA DE ROL; Clerk = pertenencia al workspace. No es invertir la federación: es separar dos preguntas que hoy están fundidas en workspace_members.role. La identidad sigue federada como la dejó F·2.
  • El dueño es un ROL, no una persona (como Snowflake), y encaja con lo anterior.
  • USE SCHEMA va al final, pero se decide antes del primer grant de objeto: cambia si el modelo es puramente aditivo o tiene prerequisitos.

Lo primero de la próxima sesión

Medir qué hay para sostener OWNERSHIP antes de elegir forma: si es una columna, una fila de privilege_grants con privilegio OWNERSHIP —como Snowflake— o las dos. La sonda equivalente ya existe para el ámbito de objeto: g0-fuente-del-ambito-objeto.ts.


5 · 🪤 Trampas nuevas de esta sesión

⛔⛔«En local pasa y en Vercel no» = el árbol sucio. El build moría con ReferenceError: Security is not defined y npm run build local PASABA: el árbol local tenía el arreglo sin commitear y el deploy sale de git. ⇒ ante esa contradicción, git status entero, lo primero. Y filtrar su salida (grep -v) escondió justo el fichero culpable
⛔⛔Keycloak: el PUT de usuario REEMPLAZA, no parchea. Mandar sólo {attributes} borró el email y el nombre ⇒ invalid_grant: Account is not fully set up, que no se parece a «le has borrado el perfil»
⛔⛔Keycloak DESCARTA los atributos no declarados en el User Profile… y devuelve 204. unmanagedAttributePolicy viene DISABLED. Sin declarar clerk_user_id, la federación queda montada, el login funciona y el claim nunca llega
⚠️El log real de un build fallido no sale de vercel --prod: sólo dice exited with 1 + «no memory problems». El bueno: npx vercel inspect <url> --logs
⚠️Un Ready de Preview no es producción. Comprobar Environment en vercel ls
⚠️Un gate mal diseñado FABRICA fallos que no existen: el de concurrencia acusó al sistema de perder datos y el defecto era suyo (reutilizaba cliente_id entre tandas)
⚠️Un censo miente por la unidad que cuenta: censar page.tsx dio 14 pantallas de 42 — esta app usa un shell con pestañas. Y contar menciones en COMENTARIOS inventó 2 huérfanas de 3
⚠️Los scripts sin import/export son globales para TypeScript y sus constantes chocan entre ficheros — el error sale en un fichero ajeno

6 · Comandos que se corren

# la superficie
npm run check:superficie                      # trinquete: 0 huérfanas
npx tsx scripts/governance/sup0-censo-de-superficie.ts

# el plano de permisos
npx dotenv -e .env.local -- npx tsx scripts/governance/g2-madurez-del-plano.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/g3-acoplamiento-de-roles.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/g0-fuente-del-ambito-objeto.ts

# el sujeto humano (KC_ADMIN_* salen de `railway variables --service keycloak --kv`)
npx tsx scripts/governance/k1-setup-sujeto-humano.ts          # idempotente
npx dotenv -e .env.local -- npx tsx scripts/governance/k2-gate-sujeto-humano.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/k3-gate-s4-en-vivo.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/f2-gate-federacion.ts

# el sustrato
npx dotenv -e .env.local -- npx tsx scripts/warehouse/p1-gate-particion-gobernada.ts
S5_ESCRITORES=12 npx dotenv -e .env.local -- npx tsx scripts/warehouse/s5-gate-reintento-occ.ts

# en frío
npx vitest run lib/governance     # 364
npx tsc --noEmit

# el runtime — NUNCA la consola de Vercel
curl -H "x-diag-key: $SPARK_BRIDGE_JWT_SECRET" https://app.paladio.io/api/diag/motor

7 · Estado del entorno

  • Producción sirve 17178a5. Sin desplegar: e439ab3, b12cb2f, 9223136 (docs y superficie; ninguno cambia comportamiento de datos).
  • Desplegar siempre desde el worktree limpio C:/tmp/carbon-deploy, y verificar por el contenido del runtime, nunca por el Ready.
  • SCAN_PLANNING_WORKSPACES encendido para 7b500f4d… · PLAN_MANDA=on · USER_AUTHZ=enforce · PLAN_AUDITOR=shadow en prod, off en local.
  • Keycloak: realm lakehouse en Railway (proyecto cozy-comfort), con el IdP clerk, el client carbon-app, los roles ws-* y el atributo clerk_user_id declarado.
  • Tablas de prueba que quedan: p0_part_clientes, p0_plain_clientes, p0_bucket_clientes, v_robustez_*, w8_plain_*.