🛡️ sandbox-labs GitHub ↗

Formato de evidencia#

Cada ejecución escribe evidence/runs/<runId>.json.

Campos principales:

  • runtime: ID, versión y versión del adaptador.
  • host: sistema, arquitectura y kernel.
  • integrity: SHA-256 de la política, del árbol de la carga, del binario que

ejecutó, y de la propia evidencia (evidenceSha256, calculado con ese campo vacío).

sandboxctl evidence verify recalcula la huella y vuelve a hashear la política y la carga. Distingue dos cosas que se confunden:

| Qué pasó | Huella propia | Hash de la carga | |---|---|---| | nadie tocó nada | ✓ | ✓ | | alguien editó el informe | ✗ | ✓ | | el código cambió desde entonces | ✓ | ✗ |

Lo tercero no es corrupción: es un informe de hace tres semanas diciendo, con razón, que ya no describe el código de hoy.

Además va firmada con Ed25519 sobre esa huella (integrity.signature), y encadenada con la anterior (previousEvidenceSha256). Cada mecanismo ve lo que el anterior no:

| Mecanismo | Detecta | |---|---| | huella | el fichero se tocó | | firma | alguien lo rehízo recalculando la huella | | cadena | alguien borró un informe entero |

La firma no es una notarización. La clave la guarda la misma máquina que ejecuta: quien tenga acceso al equipo puede firmar lo que quiera. Prueba que el informe no cambió después de escribirse, no que la ejecución ocurriera.

  • policy.requestedControls: controles obligatorios.
  • policy.effective: controles realmente aplicados por el adaptador.
  • unsupported: controles solicitados que no pudieron demostrarse.
  • limits.requested: los límites que la política pidió.
  • limits.effective: los que el runtime aplicó de verdad en esta ejecución.

Solo entra aquí lo que se tradujo en un argumento real de la línea de comandos, y cada entrada nombra el mecanismo: cgroup memory.max no es lo mismo que RLIMIT_AS, y la evidencia no puede confundirlos.

  • limits.observed: lo que la carga consumió, leído del cgroup mientras

corría. Aplicar y medir son cosas distintas.

| Campo | Del kernel | Qué responde | |---|---|---| | memoryPeakBytes | memory.peak | cuánta memoria llegó a ocupar | | pidsPeak | pids.peak | cuántos procesos tuvo vivos a la vez | | cpuUsageUsec | usage_usec de cpu.stat | cuánta CPU gastó | | oomKills | oom_kill de memory.events | si el kernel mató algo por memoria |

Va vacío cuando no hubo cgroup propio del que leer. Nunca se rellena con las cifras del cgroup de la sesión del host: serían números reales de la máquina equivocada, que es peor que no medir.

Un campo ausente significa «no se pudo leer», no «cero». La diferencia importa: cero es un hecho medido.

  • result: código de salida, motivo, duración y salida truncada.
  • violations: observaciones estructuradas.
  • plan: decisiones del adaptador.
  • artifacts: los ficheros que la ejecución produjo, con su tamaño y su hash.
  • cleanup: qué se retiró al terminar. Un sandbox que deja rastro tiene que

decirlo.

  • verdict: ¿se puede confiar en esta ejecución?, que no es lo mismo que si

terminó bien.

| Veredicto | Qué pasó | |---|---| | contained | Ejecutó con todos los controles pedidos y terminó bien | | controls-missing | Faltaron controles. Manda sobre cualquier otro resultado | | timeout | El supervisor la cortó por tiempo | | failed | Ejecutó con sus controles y falló por sus propios motivos | | not-executed | No llegó a ejecutar |

Una carga que sale con código 0 habiendo perdido un control es controls-missing: el resultado puede ser correcto y no significar nada, porque se obtuvo sin la frontera que se creía puesta.

Una evidencia planned no demuestra aislamiento. Una evidencia completed tampoco prueba por sí sola la ausencia de escape; debe acompañarse de pruebas negativas.