linebreak-gate

Compuerta de CI con cierre ante fallos para código escrito por IA: bloquea CVEs conocidos y sirve criterios de aceptación aprobados por humanos a Claude Code/Cursor/Codex a través de MCP, de solo lectura para el agente.

Documentación

linebreak-gate — la compuerta de seguridad LineBreak en el límite git/CI

Véalo en acción

Un pull request real, bloqueado de verdad: la compuerta es un check requerido, así que el botón de merge se pone gris hasta que el CVE se corrige o un humano nombrado registra una excepción.

Real pull request blocked by the LineBreak Security Gate: required check failing, merge disabled

Véalo en vivo — un PR público que puede abrir ahora mismo →

Una grabación real, sin simulaciones: el escaneo bloquea un CVE crítico con fallo cerrado, el pin se corrige, la compuerta se abre.

linebreak-gate scan blocking a critical CVE, then passing after the fix

El ciclo de especificación: un humano nombrado aprueba los criterios, check bloquea hasta que el criterio manual lleva una firma, y entonces todo pasa.

spec approve, check blocked until sign-off, then all criteria pass

Bloquea merges que llevan vulnerabilidades conocidas. Una herramienta, dos detectores — el escaneo de dependencias es gratis; la revisión con IA es la mejora Pro:

  • Escaneo de CVE de dependencias — gratis, sin llave — osv-scanner en todos los ecosistemas (npm, PyPI, Go, Cargo, Maven, …), con un respaldo npm audit para proyectos npm (cobertura solo npm y sin datos de versión instalada — la GitHub Action falla cerrada si osv-scanner no se puede instalar en lugar de degradar a él).
  • IA SAST — Pro — una revisión de seguridad LLM del código fuente propio (inyección, autenticación rota, exposición de secretos, SSRF, deserialización insegura, mal uso criptográfico) con verificación adversarial, habilitada por LINEBREAK_LICENSE_KEY (alojado, usa créditos) o ANTHROPIC_API_KEY (su propia llave, tiene prioridad). Sin una llave, el escaneo de dependencias igual corre y esta pasada se omite con un aviso.

La compuerta bloquea y puede proponer; nunca se auto-limpia por la palabra de un agente. Un humano aprueba la corrección o registra una excepción — con una razón y un aprobador — en un archivo de auditoría confirmado en git.

Este es el mismo núcleo de escaneo que impulsa el resto de la compuerta de seguridad dentro del producto LineBreak (el backend de escritorio importa este paquete), pero es completamente independiente: un equipo que nunca ha tocado nada más de LineBreak puede agregar la compuerta a su repositorio y obtener aplicación real.

Contribución y licencia. Este repositorio es la fuente publicada de linebreak-gate (Apache-2.0): cada versión llega aquí y a PyPI desde nuestro CI, y cada cambio pasó nuestra propia compuerta primero — escaneo de CVE y criterios aprobados por humanos, la misma disciplina que vendemos. Informes de errores y solicitudes de funciones: abra un issue o discusión aquí; leemos todo. PRs directos a este repositorio no se pueden fusionar (las versiones fluyen a través de nuestro pipeline de revisión), así que comience con un issue y nosotros lo tomamos desde ahí.

Inicio rápido — GitHub Actions

# .github/workflows/security-gate.yml
name: Security gate
on:
  pull_request:

permissions:
  contents: read
  pull-requests: write # for the summary comment

jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: Baktun-Studio/linebreak-gate@v1
        with:
          # fail-on: high # blocking floor; default: critical
          # Optional today; required once license enforcement is enabled.
          license-key: ${{ secrets.LINEBREAK_LICENSE_KEY }}
          # Enables the AI code review; leave unset for dependency scan only.
          anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}

La acción corre linebreak-gate scan, siempre corre report, publica un comentario en el PR (actualizado en su lugar en cada push, nunca spam), sube el informe JSON

  • artefactos de auditoría como artefacto de workflow, y falla el check según el código de salida del escaneo.

Conviértalo en un límite real: exija el check

Un trabajo de CI que se puede ignorar es un panel, no una compuerta. En su repositorio:

Settings → Branches → Branch protection rules → su rama predeterminada → "Require status checks to pass before merging" → agregue el trabajo gate (el nombre del trabajo que corre esta acción). Desde entonces, un PR que lleva un CVE crítico no se puede fusionar a través de la interfaz de GitHub.

Inicio rápido — cualquier otro CI (ejemplo GitLab)

El CLI es un paquete Python simple con códigos de salida estrictos — 0 pasa, 1 hallazgos bloqueantes, 2 error de herramienta/configuración (fallo cerrado: un bloqueo del escáner falla el pipeline, nunca es un pase limpio). Cualquier CI que respete los códigos de salida obtiene la misma aplicación:

# .gitlab-ci.yml
security-gate:
  image: python:3.11
  script:
    - pip install linebreak-gate
    - curl -fsSL -o /usr/local/bin/osv-scanner
      "$(curl -fsSL https://api.github.com/repos/google/osv-scanner/releases/latest
      | python -c "import json,sys;print(next(a['browser_download_url'] for a in json.load(sys.stdin)['assets'] if a['name'].endswith('linux_amd64')))")"
    - chmod +x /usr/local/bin/osv-scanner
    - linebreak-gate scan
    - linebreak-gate report

Marque el trabajo como requerido (sin allow_failure) y proteja la rama.

Bitbucket Pipelines y Azure DevOps

La misma compuerta en Bitbucket Pipelines y Azure DevOps: linebreak-gate ci corre escaneo + check, publica el comentario del PR y el estado del build a través de la API del proveedor, y sale con 0/1/2. Esta sección está en español para los equipos que la pilotan; el runbook paso a paso está en docs/RUNBOOK_BITBUCKET_AZURE.md en el monorepo.

La compuerta es la misma en cualquier CI. El comando linebreak-gate ci hace en un solo paso lo que la Action de GitHub hace en varios: corre el escaneo de dependencias y la revisión de código con IA (si hay llave), evalúa los criterios de aceptación aprobados con el alcance correcto (por historia en el pull request, todo el paquete en la liberación), deja la evidencia en .linebreak/ci-out/ (report.txt, criteria.txt, report.json, comment.md y los registros de auditoría) para publicarla como artefacto, publica un comentario en el pull request (actualizado en cada corrida, nunca repetido) y un estado de build, y termina con el código 0 (pasa), 1 (bloquea) o 2 (error de herramienta: la compuerta queda cerrada). El comentario tiene el mismo contenido que el de GitHub.

Sin credenciales de API, el veredicto se imprime igual, el comentario y el estado se omiten con un aviso, y el código de salida sigue bloqueando el pipeline. La compuerta nunca se abre por no poder comentar.

linebreak-gate init detecta el proveedor por el remoto de git y escribe el archivo que corresponde; --provider bitbucket|azure|all lo elige a mano. Los dos archivos que genera son exactamente los de abajo.

Bitbucket Pipelines

# bitbucket-pipelines.yml
# Python image with git and curl; the gate pins osv-scanner itself.
# Alternative: the gate's CI image built from packages/gate/Dockerfile.ci
# (osv-scanner preinstalled), pushed to a registry your workspace can pull.
image: python:3.11

definitions:
  steps:
    - step: &linebreak-gate
        name: LineBreak gate
        script:
          # osv-scanner drives the dependency scan; without it the gate fails closed.
          - curl -fsSL -o /usr/local/bin/osv-scanner https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64
          - chmod +x /usr/local/bin/osv-scanner
          - pip install --quiet "linebreak-gate>=1.13.4,<2"
          # Scan + acceptance criteria, PR comment and build status, exit 0/1/2.
          # Repository variables (Repository settings > Pipelines > Repository variables):
          #   LINEBREAK_LICENSE_KEY   optional today; required once enforcement is enabled
          #   ANTHROPIC_API_KEY       enables the AI code review (secured)
          #   BITBUCKET_ACCESS_TOKEN  repository access token, scopes pullrequest:write
          #                           and repository:write, for the PR comment and status
          - linebreak-gate ci
        artifacts:
          - .linebreak/ci-out/**

pipelines:
  pull-requests:
    "**":
      - step: *linebreak-gate
  branches:
    main:
      - step: *linebreak-gate

Variables del repositorio (Repository settings > Pipelines > Repository variables, marcadas como secured):

  • LINEBREAK_LICENSE_KEY: opcional hoy; requerida cuando se active la exigencia de licencia.
  • ANTHROPIC_API_KEY: habilita la revisión de código con IA; sin ella corre solo el escaneo de dependencias, con aviso.
  • BITBUCKET_ACCESS_TOKEN: token de acceso del repositorio (Repository settings > Access tokens) con permisos pullrequest:write y repository:write. Es lo que permite el comentario y el estado de build. Alternativa: BITBUCKET_USERNAME + BITBUCKET_APP_PASSWORD.

Protección de rama equivalente a "required check" (Repository settings > Branch restrictions, rama main): el merge check "Check the last commit for at least 1 successful build and no failed builds". Los merge checks son parte de Bitbucket Cloud Premium; con el plan Standard el build rojo se ve en el pull request y en el estado LineBreak gate, pero no impide el merge por sí solo (se apoya en revisores obligatorios).

Azure DevOps (Azure Repos + Azure Pipelines)

# azure-pipelines.yml
trigger:
  branches:
    include:
      - main
pr:
  branches:
    include:
      - "*"

pool:
  vmImage: ubuntu-latest
# Container job alternative (image built from packages/gate/Dockerfile.ci):
# container: <registry>/linebreak-gate-ci:1

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: "3.11"
    displayName: Python 3.11

  - script: |
      set -euo pipefail
      mkdir -p "$HOME/bin"
      # osv-scanner drives the dependency scan; without it the gate fails closed.
      curl -fsSL -o "$HOME/bin/osv-scanner" https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64
      chmod +x "$HOME/bin/osv-scanner"
      echo "##vso[task.setvariable variable=LINEBREAK_OSV_SCANNER_BIN]$HOME/bin/osv-scanner"
      pip install --quiet "linebreak-gate>=1.13.4,<2"
    displayName: Install linebreak-gate

  # Scan + acceptance criteria, PR comment thread and PR status, exit 0/1/2.
  # Secret variables are NOT exported automatically: map them here. An
  # undefined $(NAME) stays literal; the gate treats such values as unset.
  - script: linebreak-gate ci
    displayName: LineBreak gate
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)
      LINEBREAK_LICENSE_KEY: $(LINEBREAK_LICENSE_KEY)
      ANTHROPIC_API_KEY: $(ANTHROPIC_API_KEY)

  - task: PublishBuildArtifacts@1
    condition: always()
    inputs:
      pathToPublish: .linebreak/ci-out
      artifactName: linebreak-gate-report
    displayName: Publish the gate report

Cuatro pasos manuales, en este orden:

  1. Crear el pipeline desde azure-pipelines.yml (Pipelines > New pipeline > Azure Repos Git > Existing YAML) y agregar las variables secretas LINEBREAK_LICENSE_KEY y ANTHROPIC_API_KEY (Edit > Variables). Una variable que no existe queda como el texto literal $(NOMBRE); la compuerta la trata como no definida.
  2. Project settings > Repos > Repositories > el repositorio > Security: dar a la identidad <proyecto> Build Service (<organización>) el permiso Contribute to pull requests. Sin eso, el comentario y el estado fallan con 403 (y el pipeline sigue bloqueando por código de salida).
  3. Repos > Branches > main > Branch policies > Build validation: agregar este pipeline como Required, disparo Automatic. Esa política es la que deja el botón Complete apagado mientras el build esté rojo.
  4. Opcional: en la misma página, Status checks: exigir el estado linebreak/gate que la compuerta publica en cada pull request.

En Azure Repos el disparador pr: del YAML no aplica: la política de Build validation es la que corre el pipeline en cada pull request. pr: queda para repositorios alojados en GitHub o Bitbucket y construidos desde Azure Pipelines (en ese caso el comentario debe publicarse en ese proveedor; la API de PR de Azure DevOps no aplica y la compuerta lo dice).

Imagen Docker: dos caminos

  1. Imagen base de Python + pip install (las plantillas de arriba). Funciona hoy sin publicar nada; descarga osv-scanner desde GitHub en cada corrida (si el runner no tiene salida a internet, usar el camino 2).

  2. Imagen de CI de la compuerta, construida desde Dockerfile.ci en este directorio y publicada en un registro que el workspace o la organización pueda leer:

    docker build -f Dockerfile.ci -t <registro>/linebreak-gate-ci:1 .
    docker push <registro>/linebreak-gate-ci:1
    

    En Bitbucket: image: <registro>/linebreak-gate-ci:1 y se quitan las líneas de curl y pip install del script. En Azure: un container job (container: <registro>/linebreak-gate-ci:1 bajo pool) y se quita el paso de instalación. La imagen trae osv-scanner, git, bash y curl, corre como root y no define ENTRYPOINT: los tres son requisitos de Bitbucket Pipelines y de los container jobs de Azure.

    El Dockerfile sin sufijo es la imagen del servidor MCP (entrypoint linebreak-gate mcp, usuario sin privilegios, sin osv-scanner) y no sirve para CI.

El comando

linebreak-gate ci [--path .] [--fail-on critical|high|medium|low]
                  [--story all|auto|<id>] [--manual auto|warn|block] [--stage auto|release|pr]
                  [--out-dir .linebreak/ci-out] [--no-comment] [--no-status]

--story auto toma la historia del nombre de rama feat/<id> o story/<id> (con sufijo permitido) cuando es una historia aprobada, y si no evalúa solo las historias iniciadas. --manual auto es warn en un pull request y block en cualquier otra corrida; --stage auto es pr en un pull request y release en el resto. Los proveedores detectados son GitHub Actions, GitLab CI, Bitbucket Pipelines y Azure Pipelines; en GitHub el comentario lo sigue publicando la Action.

El ciclo de especificación — autor, aprueba, sirve sobre MCP, aplica

La compuerta también aplica criterios de aceptación aprobados, y todo el ciclo es agnóstico a la herramienta — sin cuenta LineBreak, sin app de escritorio, sin servidor:

linebreak-gate spec new        # scaffold a draft — fill it with any tool (your
                               # editor, Claude Code, ChatGPT), or distill it
                               # from the PRD you already have in Notion/Jira
linebreak-gate spec approve .linebreak/spec-draft.yml \
  --approver "Ana Lopez <ana@example.com>"   # a human on the record; commits
linebreak-gate mcp install --editor claude-code   # or: cursor · codex · copilot

mcp install registra el servidor (.mcp.json, .cursor/mcp.json, ~/.codex/config.toml, o .vscode/mcp.json para GitHub Copilot en VS Code) y escribe un bloque corto gestionado, entre marcadores LineBreak, en el archivo que el agente siempre lee: CLAUDE.md, .cursor/rules/linebreak.mdc, AGENTS.md o .github/copilot-instructions.md. El bloque dice que los criterios son el contrato firmado, se leen con las herramientas MCP (no el CLI, no .linebreak/), que los comandos de escritura del CLI necesitan que una persona pregunte, y que la especificación nunca se edita. Sigue el idioma de la especificación (español o inglés); ejecutar install de nuevo reemplaza solo ese bloque.

linebreak-gate mcp sirve el paquete aprobado (.linebreak/spec/) sobre MCP (stdio) a Claude Code, Cursor, Codex, o cualquier cliente MCP. Seis herramientas: list_stories, get_story (criterios como contexto del agente ANTES de que se escriba código), next_story, set_story_status, check_story (el mismo motor de evaluación que corre CI, con alcance a una historia), y spec_status (aprobación + estado de firma sin conexión). Git es el transporte — sin red, sin cuenta, funciona en un clon pelado — y nada en el puente puede escribir, editar o invalidar un criterio aprobado: los criterios cambian solo editando el borrador y re-aprobando, con un humano en el registro.

spec approve imprime lo que está a punto de aprobar (y cada declaración que cambió, antes y después) y rechaza un borrador que no sea el del remoto; --local aprueba la copia en disco a sabiendas.

Luego linebreak-gate check aplica los mismos criterios en CI: los checks de máquina corren de verdad, los criterios manual bloquean hasta que haya una firma registrada. Un criterio tests que ejecutó cero pruebas falla, y un check puede declarar lo que espera observar (expect: {output, exit}); la sección "Integrity" de docs/CRITERIA_ENFORCEMENT.md en el repositorio tiene los detalles. Primera ejecución guiada con el porqué de cada paso: linebreakapp.com/en/start.

CLI

linebreak-gate init     [--path .] [--fail-on critical|high|medium|low] [--force] [--non-interactive] [--provider auto|github|bitbucket|azure|all]
linebreak-gate ci       [--path .] [--fail-on ...] [--story all|auto|<id>] [--manual auto|warn|block] [--stage auto|release|pr] [--out-dir DIR] [--no-comment] [--no-status]
linebreak-gate scan     [--path .] [--fail-on critical|high|medium|low] [--format summary|json]
linebreak-gate report   [--path .] [--format summary|json]
linebreak-gate override --finding <id> --reason "…" --approver <name/email> [--expires YYYY-MM-DD|--days N] [--path .]
linebreak-gate override --criterion <id> --reason "…" --approver <name/email> [--expires YYYY-MM-DD|--days N] [--path .]
linebreak-gate check    [--path .] [--format summary|json] [--story <id> ...|--started-only] [--manual block|warn] [--stage release|pr]
linebreak-gate signoff  --criterion <id> --approver <name/email> --note "…" [--path .]
linebreak-gate tickets sync [--path .]         # retry pending ticket-tracker operations
linebreak-gate spec new     [--path .] [--out <file>] [--force]
linebreak-gate spec approve <draft> --approver <name/email> [--role architect] [--path .]
                            [--against <remote>/<branch>] [--local]
linebreak-gate spec list|next [--path .]
linebreak-gate spec show|check <story-id> [--path .]
linebreak-gate mcp      [--path .]            # serve the approved spec over stdio
linebreak-gate mcp install [--editor claude-code|cursor|codex|copilot] [--print]
linebreak-gate badge    [--format markdown|html|url]
linebreak-gate publish  --to <governance-url> [--project <id>] [--path .] [--stage pr|release] [--run-id <id>] [--dry-run]
  • init configura un repositorio con un solo comando: escribe el archivo de pipeline para el proveedor de CI del repositorio (workflow de GitHub Actions, bitbucket-pipelines.yml o azure-pipelines.yml, detectado desde el remoto de git o elegido con --provider; nunca sobrescribe uno existente sin --force), opcionalmente escribe .linebreak/gate.yml, ofrece almacenar los secretos mediante la CLI de GitHub y exigir la verificación de gate — e imprime los enlaces de configuración exactos para cualquier cosa que no pueda hacer por ti.

  • ci es la ejecución completa para proveedores de CI sin una Action nativa (Bitbucket Pipelines, Azure DevOps): escaneo, verificación con alcance, directorio de evidencia, comentario en PR y estado de build a través de la API del proveedor, saliendo con el código peor. Consulta la sección de Bitbucket / Azure arriba.

  • scan ejecuta ambos detectores, escribe artefactos de auditoría nativos de git bajo .linebreak/audit/, y sale con 0/1/2.

  • report renderiza el escaneo registrado: conteos por severidad y cada hallazgo con ID de CVE, CVSS, enlace de aviso y estado de anulación. --format json para máquinas.

  • override registra un reconocimiento aprobado por humanos de un hallazgo exacto — el paquete + versión instalada + tupla de CVE. Un CVE diferente, una versión actualizada o un hallazgo nuevo aún bloquean. --reason y --approver son obligatorios; el registro aterriza en el rastro de aprobación del artefacto. Confirma el .linebreak/audit/*.json actualizado para que CI lo vea. --expires YYYY-MM-DD (o --days N) limita la aceptación en el tiempo: después de esa fecha el hallazgo vuelve a bloquear como expired_risk hasta que la aceptación se renueve o el hallazgo se corrija (ver Riesgos aceptados y tickets).

  • check evalúa los criterios de aceptación aprobados (.linebreak/spec/, aterrizado por spec approve) contra el árbol de trabajo: build/tests/command se ejecutan de verdad, manual requiere una aprobación registrada. Sale con 0 si todo está satisfecho (o sin bundle — un no-op limpio), 1 bloqueando (fallo o requiere aprobación), 2 error de herramienta/configuración/bundle (fallo cerrado). Escribe .linebreak/audit/criteria.json. Banderas de alcance (ver Alcance de verificación): --story <id> (repetible) evalúa solo esas historias, --started-only evalúa solo historias con un estado local iniciado, --manual warn reporta aprobaciones faltantes sin bloquear, --stage pr omite criterios marcados check.when: release (listados como solo-release, no evaluados; el valor predeterminado --stage release los evalúa). El resumen y el JSON indican el alcance.

  • signoff registra una aprobación humana atribuida para un criterio manual bajo .linebreak/spec/signoffs/ (aditivo; --approver y --note obligatorios). Se vincula al criterio como aprobado — editar el criterio y re-aprobar la especificación hace que las aprobaciones anteriores queden obsoletas. Confirma el registro.

  • override --criterion registra una anulación aprobada por humanos para un criterio de máquina fallido en .linebreak/audit/criteria.json — misma filosofía que las anulaciones de CVE: posible, siempre atribuida, obsoleta una vez que el criterio se edita. Otros criterios bloqueantes aún bloquean.

  • spec new / spec approve — la ruta de autoría independiente de herramientas (ver la sección de bucle de especificación arriba): crea un borrador, llénalo con cualquier herramienta, aterrízalo como el bundle aprobado con una aprobación humana atribuida, confirmado. Las aprobaciones locales sin firmar se marcan como identity_source: client; las firmas criptográficas provienen del servicio de gobernanza (clave de licencia).

  • spec list imprime el bundle de criterios de aceptación aprobados: cada historia, sus criterios con tipos de verificación y la atribución del aprobador. Solo lectura. Sale con 0 en un bundle válido o cuando no existe ninguno; sale con 2 en un bundle malformado (fallo cerrado en estructura). spec next / show / check son los gemelos de CLI de las herramientas del puente MCP.

Publicar en el panel

linebreak-gate publish --to https://governance.example --project <id> envía la ejecución registrada (criterios, hallazgos, aprobaciones, anulaciones, atestación) a un servicio de gobernanza de LineBreak para que ejecutivos y auditores lo vean en el panel. El bearer proviene de LINEBREAK_GOV_TOKEN (un token emitido por ese servicio), si no de LINEBREAK_GOVERNANCE_TOKEN; --to por defecto es LINEBREAK_GOVERNANCE_BASE_URL y --project a LINEBREAK_GOV_PROJECT. Ejecútalo después de scan y check, como el último paso del trabajo.

Publicar nunca bloquea un cambio: un token faltante, un servicio inalcanzable o un cuerpo rechazado imprime una advertencia y sale con 0. El veredicto ya fue dado por scan/check; publish solo lo reporta. Bajo GitHub Actions, el run_id se deriva del ID de ejecución y del intento, por lo que re-ejecutar el paso es idempotente en el servidor. --dry-run imprime el cuerpo sin enviarlo.

Credenciales de gobernanza: una sola búsqueda

Cada comando que habla con el servicio de gobernanza (check para las aprobaciones del panel y la configuración del rastreador, signoff / override para la identidad verificada, publish, report --from-governance) encuentra la URL del servicio, el token y el proyecto de la misma manera, el primero que aplique:

  1. variables de entorno (LINEBREAK_GOVERNANCE_BASE_URL, LINEBREAK_GOVERNANCE_TOKEN, LINEBREAK_GOV_TOKEN, LINEBREAK_GOV_PROJECT); una variable establecida siempre gana;
  2. ~/.config/linebreak/governance.env ($XDG_CONFIG_HOME/linebreak/ cuando está establecido);
  3. ~/.config/linebreak/governance-local.env (una instancia local);
  4. ~/.linebreak/env, mantenido para compatibilidad con la aplicación de escritorio.

Los archivos son líneas KEY=value (export y comillas permitidas). Solo se usa el primer archivo que lleva un token, completo: una URL de un archivo nunca se combina con un token de otro. LINEBREAK_GOV_CREDENTIALS=off desactiva los archivos. En CI no existen tales archivos, por lo que los pipelines se comportan como antes; en una estación de trabajo, un token en uno de ellos hace que signoff y override registren la identidad de gobernanza verificada (y se nieguen si ese token falla), exactamente como lo hace la variable exportada.

Leer la ejecución publicada: report --from-governance

El escaneo se ejecuta en CI; report solo lee el escaneo registrado en esta máquina. linebreak-gate report --from-governance --project <id> lee la última ejecución que CI publicó en su lugar (GET /v1/reports/projects/{id} para la lista, GET /v1/projects/{id}/gate-runs/{run_id} para la ejecución): hallazgos por severidad con sus aceptaciones y marcas KEV, los conteos de criterios y el veredicto tal como esa ejecución lo publicó. --stage pr|release toma la última ejecución de esa etapa, --run-id una específica, --format json la ejecución completa. Solo lectura; sale con 2 cuando el servicio no se puede leer, 0 en caso contrario (también cuando aún no se ha publicado nada).

Verificar la evidencia sin red: verify

La instancia de gobierno firma con su llave Ed25519 (el mismo kid que firma la aprobación de la especificación) el expediente de cada corrida, cada excepción y cada firma hecha desde el panel. "Descargar evidencia" en el panel (o GET /v1/projects/{id}/gate-runs/{run_id}/evidence) baja un archivo linebreak-evidence/v1 con cada registro, su texto firmado, su firma, su kid y las llaves públicas de la instancia.

linebreak-gate verify evidencia-pagos-2026-09-12-55555555.json
linebreak-gate verify evidencia.json --key=<kid>=<llave pública base64>

Llaves de confianza: approvals.public_keys de .linebreak/gate.yml (en --path, por defecto .) y las --key. La llave que trae el propio archivo solo cuenta con --key-from-file, y el comando avisa que hay que confirmarla por otro canal (GET /v1/keys o gate.yml). No usa la red.

SalidaSignifica
0todo firmado por la instancia y verifica
1una firma no verifica o su kid no es de confianza
3lo firmado verifica, pero hay registros sin firma de la instancia (anteriores a esta versión)
2el archivo no se puede leer o su formato no es conocido

Badge

Muestra a los visitantes que el repositorio está protegido. linebreak-gate badge imprime un fragmento de README listo para pegar (sin llamadas de red — la insignia estática de shields.io está completamente codificada en su URL); --format html|url para las variantes de etiqueta o URL desnuda:

[![gated by LineBreak](https://img.shields.io/badge/gated%20by-LineBreak-14120F?labelColor=FAF8F4)](https://www.linebreakapp.com/en/gate)

Alcance de verificación: por historia en PRs, completo en release

Un equipo que aprueba todo el sprint por adelantado (el flujo que esta puerta promueve: especificación aprobada antes del código) vería de otro modo cada PR bloqueado por criterios de historias que nadie ha comenzado. La solución es el alcance, no una puerta más débil:

  • Escaneo siempre. Los escaneos de dependencias y código se ejecutan en cada PR y en release, sin cambios.
  • Verifica la historia en PRs. linebreak-gate check --story <id> evalúa solo los criterios de esa historia (--story se repite); --story auto toma la historia de una rama feat/<id> o story/<id>, si no solo historias iniciadas. --started-only evalúa solo historias cuyo estado local es doing, review o done (el estado que spec next y el puente MCP escriben); las historias sin estado se listan como no iniciadas y no cuentan. Cuando ninguna historia está iniciada en absoluto (sin archivo de estado, uno ilegible o un rastreador externo sin estados locales) el alcance no selecciona nada y la verificación es salida 2, nunca un pase vacío. --manual warn reporta criterios manual sin aprobación como pendientes en lugar de bloqueantes, para que una aprobación que pertenece al release no detenga un PR.
  • Verifica todo en release. El trabajo de release ejecuta el bundle completo con --manual block (el predeterminado): cada criterio de cada historia, cada criterio manual aprobado. Cada ejecución escribe .linebreak/audit/criteria.json sellado con su scope y pending_signoffs, por lo que una ejecución con alcance o relajada es evidencia de esa ejecución y nunca puede leerse como un veredicto completo (y CI nunca sube uno obsoleto).

El resumen imprime una línea scope: (modo, historias evaluadas, criterios contados, política manual, etapa), una línea release-only (not evaluated at stage pr): cuando se omitieron criterios y una línea pending sign-off: por aprobación faltante; el JSON lleva lo mismo bajo scope (incluyendo stage y release_only) y pending_signoffs. Un ID de --story desconocido es salida 2 (un alcance que no nombra nada es un error, nunca un pase).

En la GitHub Action, el mismo patrón son tres entradas (stage se describe abajo). story es all (cada historia, el predeterminado), auto (inferir el ID de una rama feat/<id> o story/<id>, con un slug final permitido como en feat/<id>-add-login, cuando es una historia aprobada; si no solo historias iniciadas), o un ID explícito. manual es warn o block; dejado vacío es warn en pull_request y block en cualquier otro evento. El comentario del PR muestra el alcance resuelto, las historias no iniciadas y las aprobaciones pendientes.

Cambio de comportamiento para usuarios existentes de @v1 (1.11.0): el predeterminado de manual en eventos pull_request ahora es warn, por lo que un criterio manual sin aprobación ya no bloquea un PR a menos que el workflow establezca manual: block. Agrega el trabajo de release abajo (o establece manual: block en el trabajo de PR) para mantener las aprobaciones aplicadas.

# .github/workflows/security-gate.yml, PR gate + release gate
name: Security gate
on:
  pull_request:
  push:
    tags: ["v*"] # the release job runs on release tags

permissions:
  contents: read
  pull-requests: write

jobs:
  gate:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: Baktun-Studio/linebreak-gate@v1
        with:
          license-key: ${{ secrets.LINEBREAK_LICENSE_KEY }}
          story: auto # this PR's story, or started stories only
          manual: warn # sign-offs are listed, not blocking, on PRs
          stage: pr # check.when: release criteria are listed, not evaluated

  release-gate:
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: Baktun-Studio/linebreak-gate@v1
        with:
          license-key: ${{ secrets.LINEBREAK_LICENSE_KEY }}
          story: all # every story, every criterion
          manual: block # every manual criterion needs its sign-off
          stage: release # the default; release-only criteria run and block

Exige la verificación de gate en la rama predeterminada y el trabajo release-gate antes de publicar. CI genérico: las mismas dos invocaciones de la CLI, con los códigos de salida respetados.

Criterios solo-release: check.when: release

Un criterio verificado por un script contra un entorno de staging compartido falla para cada PR en el momento en que staging retrocede, incluido el PR que lo corrige. Márcalo como when: release en la especificación:

- id: checkout-smoke
  statement: The checkout smoke script passes against staging.
  check:
    type: command
    payload: ./scripts/smoke-staging.sh
    when: release # absent means always

Con stage: pr en el trabajo de PR (check --stage pr), dichos criterios no se evalúan: sus verificaciones nunca se ejecutan, el informe los lista como [release-only] con su propio conteo, y no son ni aprobados ni reprobados. El trabajo de release (stage: release, el predeterminado) los evalúa como siempre, y uno que falle aún bloquea el release. El campo es parte del contenido del criterio, por lo que agregarlo a un criterio aprobado re-activa las firmas y anulaciones de ese criterio como cualquier otra edición.

Release en dos fases: check.when: attestation y --phase

Un criterio como "el release lleva una atestación firmada" se firma EN una ejecución de release en verde, por lo que la ejecución que lo produce no puede exigirlo. Marca ese criterio manual como when: attestation:

- id: release-attestation
  statement: The release carries a signed attestation of its green run.
  check:
    type: manual
    when: attestation

Luego haz el release en dos ejecuciones: check --phase prepare (entrada de Action phase: prepare) evalúa todo excepto ese criterio, que se muestra como [awaiting-attestation]; el aprobador lo firma contra esa ejecución; check (--phase verify, el predeterminado) lo aplica como cualquier criterio manual, y esa ejecución es el release. Otros criterios manual bloquean en ambas fases.

Entornos compartidos: check.resource y check.environment

- id: f1-e2e
  statement: Flow F1 end to end against staging.
  check:
    type: command
    payload: node scripts/qa/flows.mjs --base "$STAGING_URL" --flow f1
    when: release
    resource: staging-demo-merchant # runs alone; a clash is reported as such
    environment:
      name: staging
      version_url: ${STAGING_URL}/version # answers the deployed commit

Las verificaciones que nombran el mismo resource nunca se superponen en una misma máquina (un bloqueo de SO), y una que falle se vuelve a ejecutar una vez en solitario: pasar en solitario es [collision] (no es un defecto, no bloquea), volver a fallar es una falla "reproducida al re-ejecutarse en solitario". Entre máquinas, serializa los trabajos en CI o dale a cada ejecución de extremo a extremo su propio tenant desechable. environment registra la versión desplegada que la verificación midió y advierte (nunca bloquea) cuando no es el commit evaluado: atrasado, adelantado, divergido, inalcanzable o cambiado mientras la verificación se ejecutaba.

Configuración — .linebreak/gate.yml

La rigurosidad de la compuerta es gobernanza, por lo que vive en el repositorio: cambiar el umbral es en sí mismo un PR: visible, revisable, atribuible en el historial de git.

# .linebreak/gate.yml
fail_on: critical # critical (default) | high | medium | low
exclude_paths: # optional: root-relative globs excluded from scanning
  - fixtures
  - "sandbox/*"
code_scan: auto # auto (run when model credentials are set) | on (required) | off
security: # exploitation policy, see "Prioridad por explotación" below
  block_kev: true # default: anything in CISA's KEV catalog blocks
  epss_threshold: 0.5 # default: off
criteria:
  enforce: true # default: true whenever a spec bundle exists; false disables
  # criteria checking only (the security scan is unaffected)
  integrity: warn # warn (default) | block | off: shared tests between stories,
  # statements naming identifiers gone from the code, commands that ran 0 tests
risk_acceptance: # optional: accepted risks expire (see the section below)
  max_days: 90 # longest acceptance allowed
  required: true # an override without --expires/--days is refused
tickets: # optional: mirror every acceptance into your tracker
  provider: jira # jira | github
  project: SEC # Jira project key, or "owner/repo" for GitHub Issues
  labels: [linebreak]

Precedencia: bandera explícita --fail-on / entrada de Action → .linebreak/gate.yml → predeterminado integrado (critical). fail_on puede vivir en el nivel superior o bajo security: (no ambos). Una configuración inválida es un error de herramienta (salida 2): un archivo de gobernanza roto nunca cae silenciosamente a un valor predeterminado.

Prioridad por explotación

Un CVSS alto dice cuánto daño haría una vulnerabilidad; no dice si alguien la está usando. Desde 1.13.2 la compuerta enriquece cada hallazgo de dependencias con dos fuentes públicas y ordena y bloquea por explotación real:

  • KEV (catálogo de vulnerabilidades explotadas conocidas de CISA): "la están explotando hoy".
  • EPSS (FIRST): probabilidad de explotación en los próximos 30 días, entre 0 y 1: "probable en 30 días".

Política

# .linebreak/gate.yml
security:
  fail_on: high # piso de severidad (o en la raíz del archivo, no en ambos)
  block_kev: true # cualquier hallazgo en KEV bloquea, sea cual sea su severidad (por defecto)
  epss_threshold: 0.5 # bloquea desde esta probabilidad a 30 días (apagado por defecto)
  intel_max_age_hours: 24 # antigüedad máxima de la caché (por defecto 24 h)
  intel: true # false apaga el enriquecimiento por completo (red y caché)

Un hallazgo bloquea por cualquiera de tres disparadores: severidad en o sobre el piso, presencia en KEV (con block_kev) o EPSS en o sobre el umbral. El motivo queda registrado en cada hallazgo (block_reason) y agregado en la corrida (block_reasons) con exactamente dos valores, que el servicio de gobierno lee por separado: kev (en el catálogo) y vulnerability (piso de severidad o umbral EPSS). Cuando aplican los dos, gana kev. Un override registrado con linebreak-gate override --finding <id> reconoce el hallazgo exacto igual que siempre, cualquiera sea el motivo.

Orden y puntaje

El report, el veredicto y el JSON listan los hallazgos en este orden: primero los que están en KEV, luego por EPSS de mayor a menor, luego por severidad y CVSS. Un hallazgo sin puntaje EPSS (sin CVE, o un CVE que FIRST aún no puntúa) va después de los puntuados, ordenado por severidad. Cada uno muestra su etiqueta: explotada activamente (KEV), EPSS 0.93 o sin datos de explotación.

  [BLOCKING: kev] CVE-2021-44228  critical cvss 10.0  log4j-core@2.14.1
      explotada activamente (KEV), EPSS 1.00, CISA due 2021-12-24  risk 100
  [BLOCKING: vulnerability] CVE-2024-3094  critical cvss 10.0  xz@5.6.0
      EPSS 0.86  risk 100
  [below floor] CVE-2019-0001  low cvss 2.0  x@1
      EPSS 0.03  risk 20

El risk_score sube con la explotación y nunca baja: un hallazgo en KEV vale 100 sin importar su severidad; el EPSS eleva el puntaje hasta su probabilidad en porcentaje (un medium con EPSS 0.93 vale 93); la severidad es el piso (critical 100, high 80, medium 50, low 20). Sin datos de explotación los números son los de siempre.

Caché y modo sin red

Las dos fuentes se guardan en .linebreak/cache/ (la carpeta se ignora sola en git; el registro de auditoría es .linebreak/audit/security.json, no la caché). Con caché fresca no hay ninguna llamada de red. Con caché vencida se consulta la red y, si falla, se usa la caché vencida y el reporte lo dice (stale). Sin red y sin caché los campos epss y kev quedan vacíos (null) y el reporte dice sin datos de explotación: el enriquecimiento nunca convierte un escaneo en error ni cambia el veredicto que habría dado sin datos. LINEBREAK_OFFLINE=1 salta la red (la caché sigue usándose), útil en pipelines sin salida a internet; security.intel: false apaga todo.

En el artefacto, kev: true es "está en el catálogo", kev: false es "se consultó el catálogo y no está" y kev: null es "no se pudo consultar" (o el hallazgo no tiene CVE): para un auditor son tres hechos distintos. La corrida registra además exploit_intel (fuente y versión del catálogo de esa corrida) y verdict con sus block_reasons.

Fuentes: https://api.first.org/data/v1/epss (por lotes de CVE) y https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json. Ninguna requiere credenciales.

Roles e identidad de quien firma (.linebreak/roles.yml)

Sin este archivo, cualquiera puede correr signoff u override y el nombre que queda en el registro es el que la persona escribió (identity_source: client). Con él, la compuerta sabe quién puede firmar qué:

# .linebreak/roles.yml
roles:
  ciso:
    members: [ana@example.com]
    can:
      sign_criteria: ["*"] # patrones sobre el id del criterio, la historia o la épica
      approve_overrides: ["*"]
      accept_security_risk: [critical, high, medium, low]
  qa:
    members: [luis@example.com, "github:luis-qa"]
    can:
      sign_criteria: ["e12-*"]
      approve_overrides: []
      accept_security_risk: [low, medium]
policy:
  require_roles: true # una firma sin rol autorizado se rechaza
  require_verified_identity: false # true: una identidad solo declarada no cuenta
  • signoff y override registran el rol con el que se firma: --role, o se infiere cuando la persona tiene exactamente un rol que lo permite. Si no tiene ninguno y require_roles está activo, el comando falla y dice qué rol haría falta. Aceptar un hallazgo de seguridad se limita por severidad.
  • check, scan y report vuelven a evaluar cada firma y cada override contra los roles vigentes. Un registro cuyo rol ya no existe, cuyo miembro salió, o que no cubre ese criterio, se rechaza con motivo role_denied y una línea legible (role denied: <id> (<historia>): ...); bloquea aunque el check corra con --manual warn. El registro no se borra: queda como evidencia y una firma posterior autorizada lo reemplaza.
  • Identidad: además de client, la compuerta reconoce vcs (en CI toma el actor del proveedor: GITHUB_ACTOR, GITLAB_USER_EMAIL, BITBUCKET_STEP_TRIGGERER_UUID, BUILD_REQUESTEDFOREMAIL) y governance (con LINEBREAK_GOVERNANCE_BASE_URL y LINEBREAK_GOVERNANCE_TOKEN consulta GET /v1/me y usa esa identidad; un token configurado que falla detiene el comando, nunca cae en silencio a un nombre escrito). La identidad verificada manda; lo que se escribió en --approver se guarda como declared_approver. En la lista de miembros se puede poner un correo o la forma proveedor:login (github:luis-qa).
  • policy.require_verified_identity: true hace que una firma con identity_source: client se registre como declarada pero no cuente: el comando lo avisa al firmar y check la rechaza con motivo identity_unverified.
  • Sin archivo, o con ambas políticas en false, nada cambia: nada se rechaza y los registros llevan role: null. Un archivo malformado es error de configuración (exit 2), nunca una omisión silenciosa.

Registros de auditoría

Cada escaneo y cada anulación se registran en .linebreak/audit/security.json (dependencias) y .linebreak/audit/code.json (AI SAST): el mismo formato de documento versionado que escriben las herramientas de LineBreak, que lleva hallazgos (ID de CVE, CVSS, enlace de aviso), motor de escaneo, marca de tiempo, actor y la trazabilidad de aprobación con el motivo y aprobador de cada anulación, el rol bajo el que se hizo y la fuente de identidad (client, vcs, governance). Quién relajó la compuerta, y cuándo, es en sí mismo auditable.

Riesgos aceptados y tickets

Una excepción registrada con override era para siempre: la persona que aceptó el riesgo se va y el riesgo se queda. Desde la versión 1.13.3 cada aceptación puede (o debe) vencer, y cada excepción vive también en el gestor de tickets del equipo, para que la trazabilidad quede en su herramienta y no solo en LineBreak.

Vencimiento

linebreak-gate override --finding <id> --reason "…" --approver ana@example.com --expires 2026-12-31
linebreak-gate override --criterion <id> --reason "…" --approver ana@example.com --days 30
  • --expires YYYY-MM-DD o --days N fijan la fecha en que la aceptación deja de valer. Vale hasta ese día inclusive; al día siguiente el hallazgo (o el criterio) vuelve a bloquear con motivo expired_risk. El informe dice qué hallazgo es, quién lo aceptó, cuándo venció y las dos salidas: renovar con otro override o corregir.

  • Con menos de 14 días por vencer, scan, report y check avisan sin bloquear (expiring risk / expiring exception).

  • La política vive en .linebreak/gate.yml:

    risk_acceptance:
      max_days: 90 # ninguna aceptación puede ir más allá de 90 días
      required: true # un override sin vencimiento se rechaza (exit 2)
    

    Sin este bloque, las aceptaciones sin fecha siguen permitidas (el comportamiento anterior). La misma política aplica a hallazgos de seguridad y a excepciones de criterios.

  • Renovar es registrar un override nuevo sobre el mismo objetivo. La aceptación anterior no se pisa: queda en el historial del artefacto (.linebreak/audit/security.json, code.json, criteria.json) y la más reciente es la que rige. Cada entrada guarda expires, y ticket cuando hay gestor configurado.

  • En --format json, scan/report traen block_reasons (vulnerability, expired_risk) y, por detector, expired y expiring; check trae block_reasons, expired_overrides y expiring_overrides.

Tickets (Jira primero, GitHub Issues también)

# .linebreak/gate.yml
tickets:
  provider: jira # jira | github
  project: SEC # clave del proyecto en Jira, o "owner/repo" para GitHub Issues
  labels: [linebreak] # etiquetas que llevan todos los tickets
  main_branch: main # rama cuyos scans abren tickets de atención (opcional)
  issue_type: Task # solo Jira: tipo de incidencia a crear (opcional)

Credenciales por entorno, nunca en el repositorio: JIRA_BASE_URL, JIRA_EMAIL y JIRA_API_TOKEN para Jira (API REST v2, funciona en Cloud y en Data Center); GITHUB_TOKEN para GitHub Issues.

Qué hace la compuerta cuando hay un tickets: configurado:

MomentoQué pasa en el gestor
override acepta un hallazgo o excusa un criterioCrea un ticket con el hallazgo o criterio, quién aceptó, motivo, vencimiento, repositorio, commit y enlace al registro de evidencia. La clave queda en la entrada de evidencia (ticket: "SEC-123"). Si ya existía, lo comenta y lo reabre (renovación).
scan o check ven una aceptación vencidaComenta el ticket y lo reabre si estaba cerrado. Un solo comentario por fecha de vencimiento, aunque el gate corra en cada push.
El hallazgo desaparece del scan, o el criterio pasa por sí solo en un check completoComenta y cierra el ticket.
Un scan en la rama principal encuentra un hallazgo que no estaba en el escaneo anterior guardadoAbre un ticket de atención (vulnerabilidad nueva sobre código ya liberado).

Detalles que conviene saber:

  • El gestor nunca bloquea el veredicto. Sin red, credenciales malas o un proyecto que rechaza la incidencia: el veredicto es el que dicen los artefactos, se imprime un aviso, el error queda en la evidencia (ticket_error en la entrada) y la operación queda pendiente en .linebreak/audit/tickets.json. linebreak-gate tickets sync reintenta lo pendiente (exit 0 cuando no queda nada; exit 1 si algo sigue pendiente).
  • La idempotencia vive en el gestor. Cada ticket lleva las etiquetas linebreak-target-<hash> y linebreak-artifact-<security|code|criteria>; antes de crear, la compuerta busca por etiqueta. Así un runner de CI que no tiene el libro local nunca duplica tickets, y puede cerrar los de hallazgos corregidos.
  • "Nuevo" en la rama principal se mide contra el artefacto anterior en .linebreak/audit/ (el que estaba comprometido antes de reescribirlo). El primer scan de un repositorio no abre nada: no hay línea base. Solo cuentan hallazgos a la altura del umbral fail_on o por encima, y que no tengan ya un ticket. En GitHub Actions la rama se lee de GITHUB_REF_NAME; un pull request (GITHUB_HEAD_REF) nunca cuenta como rama principal.
  • Conviene comprometer .linebreak/audit/tickets.json junto con el override (igual que security.json), para que el historial del ticket viaje con la evidencia.

Precios

Gratis, para siempre: el escaneo de vulnerabilidades de dependencias y todo el ciclo de especificación — autoría, aprobación humana, servicio MCP y aplicación en CI. Sin clave, sin cuenta.

Pro — $99/mes por equipo (precios): aprobaciones firmadas criptográficamente y a prueba de manipulación (Ed25519, verificables sin conexión), modo de aplicación con clave obligatoria y revisión de código con IA alojada sin clave de API que gestionar. Compra en la página de precios — tu LINEBREAK_LICENSE_KEY llega por correo en segundos (es la entrada license-key de la Action). ¿Prefieres tu propia clave de modelo? ANTHROPIC_API_KEY también habilita la revisión con IA; la revisión alojada de Pro es la ruta sin configuración.

El gate se ejecuta abierto por defecto: funciona sin clave e imprime un aviso cuando no se establece LINEBREAK_LICENSE_KEY (suprimido para usuarios BYOK). Eso es freemium — el escaneo de dependencias se ejecuta gratis. Los equipos que quieran exigir una clave Pro válida para que el gate se ejecute en absoluto pueden optar por LINEBREAK_ENTITLEMENTS_PROVIDER=remote, que verifica el derecho antes de cualquier escaneo y falla de forma cerrada ante una clave faltante, inválida o revocada, un plan incorrecto o un servicio inalcanzable — bloqueando todo el gate, incluido el escaneo de dependencias.