Documento originalAbrir .md

SIM-3 — resultados medidos

Estado: implementado y verde en un host. Fecha: 2026-09-10. Diseño: SIM3-DESIGN.md. Contrato superior: ECHO-3-ACCEPTANCE.md. Informe: lab/echo3_sim3_report.json · report_sha256 = 2d4fc12df82a91d3d772e5e991950d0be8e989e9e43deb88764c8cb8f9a5f007. Auditoría: lab/echo3_sim3_audit.json.

Hashes de cierre de la corrida vigente:

Artefacto SHA-256
Contenido lógico del informe, excluido su campo report_sha256 2d4fc12df82a91d3d772e5e991950d0be8e989e9e43deb88764c8cb8f9a5f007
Bytes de lab/echo3_sim3_report.json 924ba5a86a0a45e4b9b787df56dbc17873ae55ec2244b71e5f0cc402459582b6
Bytes de lab/echo3_sim3_audit.json 73384363e2f0e0fee73021619ff21396c2defa38e74cd520fa21e9ed97c1c1a3
Lock lógico de runtime 80ef5a38853b6b92d4b21093857f7cda6d0a7966f259764475e3668604fdbf2a
Bytes de lab/sim3/runtime-lock.json 0491847fb2cc62a2ab0255d7f90c4f3cefa0b1cbb0662ab0eb59d04f950a19f6
Inventario de código, 29 archivos 00c4061af6cefabc511380706734f756cbef39deb90d3ef7b3a81b6895752346

Este documento dice lo que se midió. Lo que SIM-3 no mide está en §7 y en el campo limitations del informe; no se resume aquí en positivo.


1. Qué se ejecutó

cd /home/roger/REALRXOS/RXos/echoai

PYTHONPATH=.. python3 -m echoai.nexus0.sim3 validate \
    --package docs/examples/sim3 --protocol docs/examples/sim3/protocol.json
PYTHONPATH=.. python3 -m echoai.nexus0.sim3 preflight \
    --output lab/sim3/runtime-lock.json
PYTHONPATH=.. python3 -m echoai.nexus0.sim3 bench --rehearsal \
    --output lab/echo3_sim3_report.json
PYTHONPATH=.. python3 -m echoai.nexus0.sim3 audit lab/echo3_sim3_report.json

PYTHONPATH=.. python3 tests/test_sim3.py            # 44 tests, sin backend
PYTHONPATH=.. python3 tests/test_sim3_backend.py    # 9 tests, con backend

bench exige el lock congelado antes de medir y se niega a correr si el código ha cambiado desde que se congeló (lock_stale). Devuelve 0 sólo si sim3_green es cierto; audit devuelve 0 sólo si la evidencia reconstruye y el informe era verde.

Salidas reales:

preflight exit=0
bench exit=0        # 624,8 s de reloj de pared
audit exit=0
tests/test_sim3.py          Ran 44 tests in 71.583s  OK
tests/test_sim3_backend.py  Ran  9 tests in 85.579s  OK

validate sobre el ejemplo público:

{
  "valid": true,
  "protocol_sha256": "b3be07e1d4933a716fce9207bfdda33dcc799ec413e5f749c56b859ab86f6c45",
  "package": {
    "sdf_sha256": "341c928d85251615b01878a9172d8bd4e7bf971986f7e40b438929471836ddab",
    "manifest_sha256": "facfe60ff0db8306f9801a0b67d336986b381b42d394b09e77c895ae2281edb2",
    "package_sha256": "b698df43a65257a2d6307663aca8cf8e09a2ec5ecab643e613d04daf118e3822",
    "fingerprint_sha256": "18c2efb91a497d2f12725ce563bd0a7e60e13a4913278fa89f90cdbb5b62a1f4",
    "n_models": 3, "n_barriers": 1
  },
  "roles": {"m0": "floor", "m1": "barrier", "m2": "charging_station"},
  "spawn_mm": [-3000, 0, 1000],
  "rig_start_mm": [-3000, 0, 1000],
  "approach_mm": [3000, 0, 550],
  "connectivity_attach_nodes": [125, 50],
  "libsdformat": {"tool": "gz sdf -k (libsdformat)", "returncode": 0,
                  "valid": true, "output": "Valid."}
}

2. Runtime realmente usado

lab/sim3/runtime-lock.json, lock_sha256 = 80ef5a38853b6b92d4b21093857f7cda6d0a7966f259764475e3668604fdbf2a, code_sha256 = 00c4061af6cefabc511380706734f756cbef39deb90d3ef7b3a81b6895752346 (29 archivos: los módulos de nexus0/sim3/, los tres assets, el Containerfile y los cinco esquemas).

Esta es la segunda corrida. La primera cerró igual de verde bajo el lock 2da89991… / código 9880e579…; la capa de manifiestos y runners de SIM3-RUNNERS.md cambió el inventario de código, así que el banco se repitió entero bajo lock nuevo en vez de reetiquetar aquel verde. Las medidas de física coinciden dígito a dígito entre las dos corridas.

Pieza Versión
Host Fedora Linux 43, kernel 7.1.4-100.fc43.x86_64, x86_64, Python 3.14.6
Contenedor localhost/echoai-sim3-gz:harmonic, imagen 97893d2e96e6…, base ubuntu:24.04@sha256:a61567bd…
Simulador gz-sim 8.15.0 (libgz-sim8 8.15.0-1~noble, gz-sim8-cli ídem)
SDF libsdformat14 14.9.0-1~noble
Física libgz-physics7-dartsim 7.8.0-1~noble
Transporte libgz-transport13 13.6.0-1~noble, libgz-msgs10 10.4.0-1~noble
Bindings python3-gz-sim8, python3-gz-transport13, python3-gz-msgs10, Python 3.12.3
Sandbox bubblewrap 0.11.0

--pull=never en todas las invocaciones y GZ_FUEL_STATIC_ONLY=1 en la imagen: el banco no descarga nada. latest no aparece en ningún sitio. Una plataforma distinta exige lock nuevo y repetición del banco; el auditor rechaza un informe cuyas ejecuciones no citen todas el mismo lock.


3. Los diez grupos

Todos en pass; sim3_green=true.

Grupo Medido
packages 12/12 validan con el validador propio y con gz sdf -k real
distinction 3 bases no equivalentes bajo traslación, reflexión y giros de 90°; 12 huellas distintas; 12/12 reproducibles regenerando en orden inverso
physics 24/24 avanzan 1.000 pasos con reloj leído del backend
repetition 12/12 parejas: mismo hash del SDF efectivo y 0,000 mm de diferencia en pose final (criterio ≤2 mm)
isolation 24/24: 22 accesos prohibidos denegados, 1.001 ticks entregados, 0 canarios visibles
rename 12/12: huella idéntica, transcript de ticks idéntico, 0 µm de diferencia en pose final, bytes del SDF distintos
moved_wall 12/12: huella y bytes cambian, la entidad se mueve 500,000 mm en el backend, la esfera reposa sobre el muro y no sobre el suelo, transcript idéntico
sealing 10/10 comprobaciones de compromiso, derivación, apertura única, reapertura, crash, exclusión mutua e inventario
mutants 26 casos: 3 controles positivos, 22 mutantes que fallan cada uno por su causa, 1 puntero al auditor
regression echoai.tests.test_nexus0 en verde, returncode=0

3.1 Física

En las 24 ejecuciones principales, con el rig soltado en la columna libre verificada por el evaluador:

caída a 200 ms      197,181 mm      criterio 150–250 mm
error en reposo       1,206 mm      criterio ≤10 mm
primer contacto     441 ms          par sim3_rig::body::solid / m<floor>::body::solid
mensajes de contacto    560         sensor de contacto del rig
gravedad del backend  0 0 -9,81 m/s²  leída del ECM, no del archivo
frames                1000 + 1 tick inicial = 1001

Los 24 valores de caída y de error en reposo son idénticos porque la columna del rig está libre en los doce mundos: esta prueba certifica que el backend corrió y que la esfera cayó y tocó, no que los mundos difieran. La diferencia geométrica la certifican distinction y moved_wall.

repetition da 0,000 mm: en este host y bajo este lock la física repite exactamente. Es más fuerte que el criterio de 2 mm y no se extiende a otras plataformas; SIM-3 no afirma identidad bit a bit entre hosts.

3.2 Pared movida

Ejemplo del primer par (los doce se comportan igual):

desplazamiento           +500 mm en x
pose de entidad          delta 500000 µm en x, 0 en y, 0 en z   (leída del ECM)
reposo sobre el muro   2 649 638 µm   esperado 2 650 000 µm
reposo en el original     48 941 µm   esperado    50 000 µm
transcript de ticks     idéntico al del mundo original

La sonda de colisión suelta el rig sobre la columna de la barrera desplazada en los dos mundos. En el mundo movido la esfera se detiene sobre el muro; en el original cae al suelo. Es colisión del backend, no lectura del SDF.

3.3 Aislamiento

El consumidor corre bajo bubblewrap con espacios de nombres propios de usuario, IPC, PID, red, UTS y cgroup; /usr de sólo lectura, /work en tmpfs, el resto remontado de sólo lectura, y --as-pid-1 para que PID 1 sea el propio consumidor. argv y env son los mismos en A, B y C:

argv          ["/probe/probe.py"]                      (exec: /usr/bin/python3 /probe/probe.py)
env           HOME=/work LANG=C.UTF-8 PATH=/usr/bin PWD=/work
hostname      sim3
cwd           /work
raíz visible  bin dev lib lib64 probe proc sbin usr work
procesos      1

En cada ejecución el probe intenta y se le deniega: /home, /home/roger, /root, /var, /var/tmp, /run, /run/user, /etc, /etc/passwd, /etc/shadow, /sys/class/net; escrituras en /, /probe, /usr y /proc (EROFS); TCP a 127.0.0.1:11345, :11317 y :8080; multicast UDP de descubrimiento de gz; y sockets UNIX de D-Bus y X11. 22 denegaciones por ejecución, 24 de 24.

Lo que sí puede leer es su propio espacio de nombres, y el evaluador comprueba que esté vacío: /proc/1/cmdline es su propio comando, /proc/1/environ sólo tiene las cuatro variables de la lista, /proc/net/tcp y /proc/net/unix sólo la cabecera, /dev/shm vacío.

Tres canarios privados distintos por ejecución —uno en el entorno del contenedor de Gazebo, uno en su argv y uno en un archivo privado— no aparecen en nada de lo que el consumidor ve. El probe no sabe cuáles son: informa lo que ve y decide el evaluador.

Hallazgo real durante la implementación, corregido y por eso registrado: sin --as-pid-1, PID 1 dentro de la jaula era el propio bwrap y su /proc/1/cmdline entregaba al consumidor la ruta absoluta del repositorio y el entorno del operador. Sin --remount-ro /, el consumidor podía escribir en / y /probe. Ambas fugas las detectó el probe adversarial, no una inspección de clases.

3.4 Mutantes

De paquete, cada uno desde un control positivo que validó: sdf_hash_changed → sdf_hash_mismatch, role_nonexistent → roles_model_absent, role_missing → roles_model_unassigned, role_duplicate → roles_duplicate_model, json_duplicate_key → json_duplicate_key, seed_split_leaked → manifest_schema, remote_include → sdf_unsupported_element, plugin_injected → sdf_unsupported_element, xml_dtd → sdf_doctype, symlink_out_of_package → package_symlink_entry, spawn_inside_solid → spawn_inside_solid, world_over_1mib → sdf_too_large, goal_unreachable → goal_unreachable, y una copia renombrada que conserva la huella y cambia los bytes.

De ejecución, desde una sesión de control que sí cerró: backend_disconnected → backend_no_ready con 0 ticks emitidos; stats_without_advance → clock_not_monotonic (un backend real cuyo reloj se congela); sandbox_failure → sandbox_unavailable; consumidor abusivo con campo extra, mensaje sobredimensionado y secuencia equivocada → ack_mismatch, consumidor mudo → ack_timeout, y en los cuatro casos ninguna entidad nueva aparece en el mundo; y un tick al que el supervisor añade split y seed → el consumidor lo rechaza con unexpected_fields y la sesión muere.

3.5 Sellado

Con el secreto público de ensayo de 32 bytes 0x01:

compromiso del protocolo  aea59bf8d1fa915a01ffb9f8fdd209a765d3607b4d648e27b68d0534c67e3b66
                          = SHA256(b"SIM3/v1/commit\0" + digest + secreto)
derivación de semillas    HMAC-SHA256, 12 semillas distintas en A/B/C
inventario                compromiso con salt de 32 bytes 0x02, 12 entradas
sealed → opened → consumed, reapertura rechazada  (seal_already_consumed)
crash tras abrir → queda consumed                 (seal_crash_consumed)
dos procesos abriendo a la vez → rechazado        (seal_busy)
inventario alterado → detectado                   (inventory_mismatch)

El secreto es público a propósito: prueba la maquinaria y no prueba nada sobre custodia. No existe ninguna partición C de campaña en este árbol y no se ha abierto ninguna. Un hash no es custodia: el registro de accesos y el custodio viven fuera de este repositorio.


4. Auditoría independiente

evidence_valid   true
checks           416
findings         []
recomputed_runs  24

El auditor no lee sim3_green. Regenera los doce paquetes desde las semillas públicas, comprueba que los package_sha256 y las huellas del informe son los suyos, re-hashea las 72 trazas, y recalcula por su cuenta caída, error en reposo, número de frames, ticks y contactos de las 24 ejecuciones principales antes de compararlos con lo declarado. Después verifica el compromiso del protocolo, el inventario de código y que el lock del disco sea el que el informe cita.

--self-test muta copias del informe y exige que cada una sea rechazada, partiendo de un control positivo que aceptó:

positive_control              aceptado          (sin este, lo de abajo no prueba nada)
flipped_case_result           rechazado         recompute/…/verdict, group_consistency/physics
bumped_contact_counter        rechazado         recompute/…/n_contact_msgs
rewritten_trace_hash          rechazado         trace_sha/…
different_runtime_lock        rechazado         lock_matches_report
one_world_on_another_runtime  rechazado         single_runtime_lock
green_without_groups          rechazado         sim3_green_consistent
altered_measurement           rechazado         recompute/…/fall_mm
wrong_protocol_hash           rechazado         protocol_sha256
wrong_code_hash               rechazado         code_sha256

Cada mutante re-sella el report_sha256, como haría quien manipulase el informe de verdad: reescribir informe y hash a la vez no equivale a evidencia.


5. Métricas descriptivas

Sobre las 24 ejecuciones principales, en este host:

Magnitud min p50 max
Lanzamiento (s) 0,402 0,417 0,439
RTF (s sim / s reloj) 0,300 0,305 0,308
Latencia ACK p50 (ms) 0,197 0,204 0,212
Latencia ACK p99 (ms) 0,282 0,317 0,355
RSS máx. Gazebo (KiB) 115 864 119 276 122 516
RSS máx. supervisor (KiB) 34 228 34 228 34 228
RSS máx. consumidor (KiB) 11 496 11 752 11 752

1.001 muestras de latencia por ejecución. Son descriptivas: 24 ejecuciones en una máquina no prometen una cola P99 estable, no miden ventaja edge y no permiten hablar de julios. El RTF de 0,3 es el coste del lockstep de un paso por tick a través de dos tuberías y un socket, no un límite del simulador.

Episodios totales: 72 (24 principales + 12 renombrados + 12 con pared movida + 24 sondas de colisión). Los pares metamórficos y las sondas no entran en el conteo de las 24 principales ni en la tabla de arriba.


6. Cómo está construido

Custodio (Nav)   protocolo, partición, secretos     — fuera de este árbol
        │
Supervisor (host)  paquete privado, SDF efectivo, canarios
        │
        ├── podman --network=none --read-only --pull=never
        │     └── gzrunner.py  ← único proceso que habla con Gazebo
        │           lockstep: un paso por petición; devuelve sim_time y pose
        │
        └── bwrap --unshare-{user,ipc,pid,net,uts,cgroup} --remount-ro / --as-pid-1
              └── probe.py     ← consumidor: stdin/stdout = socket, stderr = informe
                    recibe {"v":1,"seq":n,"sim_time_ns":t} y responde {"v":1,"ack":n}

El supervisor no tiene ninguna rama que dependa de la sala, la semilla o la partición: session.py, sandbox.py y rig.py no mencionan split, seed ni package_id en ninguna decisión. El tick cabe en 256 bytes y lleva tres campos; si Gazebo no arranca o deja de avanzar no hay tick que enviar, porque no existe camino de código que fabrique uno.

El SDF fuente es datos y el SDF efectivo lleva código: se validan con puertas distintas y se hashean por separado. El perfil sim3-boxes-v1 rechaza el SDF efectivo, y hay un test que lo comprueba.


7. Lo que esto no demuestra

Del campo limitations del informe, sin suavizar:

  • Sólo infraestructura: no hay vuelo, sensores, WSP desde física, aprendizaje ni córtex. agent_in_gazebo=false, cortex_calls=0.
  • El consumidor es un probe instrumental, no un agente cognitivo.
  • El aislamiento cubre fugas accidentales y accesos directos del probe. No es una afirmación sobre escapes de kernel ni sobre canales laterales temporales.
  • Contacto y caída prueban gravedad y colisión. No prueban aerodinámica ni nada sobre un X500.
  • El tick inicial lleva el tiempo del backend en su primer pre_update menos un paso, verificado como exactamente step_ns; los otros 1.000 llevan un tiempo leído del backend.
  • La conectividad es un flood fill discreto de 100 mm sobre AABB inflados, no una garantía de navegación continua segura.
  • 24 ejecuciones en un host describen latencia y RTF; no prometen P99 ni energía.
  • rehearsal-C es un ensayo con secreto publicado: no es evidencia reservada y no consume ningún examen de campaña.
  • No se afirma identidad bit a bit de la física entre hosts.
  • hardware_status="not_available". No hay AKD1000 ni AKD1500 en el laboratorio.

8. Dependencias del núcleo que SIM-3 no ha tocado

Siguen abiertas, tal como las dejó el diseño:

  • nexus0/loop.py:Agent consume env.cell() y Cell.kind. Entregarle un wrapper del mundo expondría verdad global; SDF → Cell(kind=GOAL) no se admite como percepción. Queda para GROUND-1.
  • greedy=True evita exploración pero Agent.turn() sigue actualizando T/Q y puede crear memoria/PATTERN: no equivale a congelar aprendizaje. Queda para TRANSFER-3.
  • La configuración vital actual impide combinar planificación y supervivencia. Queda para COMPOSE-1.

Ninguna de las tres se ha corregido aquí, ni a escondidas ni de frente.


9. Relevo

Manifiestos y runners cerrados: SIM3-RUNNERS.md. Siguiente fase software: FLIGHT-1S.

Vista previa

Abrir original