Una patch generata dall'IA può superare i test e tuttavia modificare una risposta API, toccare un file estraneo all'incarico o indebolire un'asserzione. La proposta di Akshat Raj è richiedere che ogni modifica sia accompagnata da un rapporto esplicito: quali controlli sono stati superati, quali sono falliti e cosa è rimasto senza verifica.
Costruiremo questa barriera con Python e Git. Non dimostrerà che il programma sia corretto; farà però dipendere l'accettazione da evidenze riproducibili, non dalla fiducia dell'agente nel proprio lavoro. La barriera confronterà una revisione di base con il commit attuale, limiterà i file modificati, eseguirà un comando di test e scriverà un rapporto JSON.
flowchart TD
A["Parche en HEAD"] --> B["Comparar con base"]
B --> C["Revisar alcance y diff"]
C --> D["Ejecutar pruebas"]
D --> E["Guardar informe JSON"]
E --> F["Aceptar o rechazar"]Preparare il repository e la barriera
L'esempio è pensato per Python 3.10 o versioni successive e per un repository Git. Non richiede API di modelli né pacchetti esterni. Il confronto viene effettuato tra la revisione indicata e HEAD; pertanto, prima di eseguire la barriera, occorre includere in un commit la patch da verificare. Le modifiche non confermate restano escluse dal confronto.
Salva il programma seguente come proofgate.py nella radice del repository. Il comando di test viene passato al programma come argomenti ed eseguito senza ricorrere a un interprete di shell.
#!/usr/bin/env python3
"""Comprobaciones básicas para un parche Git y un comando de pruebas."""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import time
from pathlib import Path
def run_command(command: list[str], timeout: int) -> dict:
"""Ejecuta un proceso sin shell y conserva una salida acotada."""
started = time.monotonic()
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout,
check=False,
shell=False,
)
return {
"command": command,
"exit_code": result.returncode,
"stdout": result.stdout[-12000:],
"stderr": result.stderr[-12000:],
"timed_out": False,
"duration_seconds": round(time.monotonic() - started, 3),
}
except subprocess.TimeoutExpired as exc:
stdout = exc.stdout or ""
stderr = exc.stderr or ""
if isinstance(stdout, bytes):
stdout = stdout.decode(errors="replace")
if isinstance(stderr, bytes):
stderr = stderr.decode(errors="replace")
return {
"command": command,
"exit_code": None,
"stdout": stdout[-12000:],
"stderr": stderr[-12000:],
"timed_out": True,
"duration_seconds": round(time.monotonic() - started, 3),
}
def git(*arguments: str) -> dict:
return run_command(["git", *arguments], timeout=30)
def main() -> int:
parser = argparse.ArgumentParser(description="Verifica un parche Git.")
parser.add_argument("--base", required=True, help="Revisión base de Git.")
parser.add_argument("--max-files", type=int, default=20)
parser.add_argument("--allow-prefix", action="append", default=[])
parser.add_argument("--test-timeout", type=int, default=300)
parser.add_argument(
"--test", nargs=argparse.REMAINDER,
help="Comando de pruebas y argumentos; debe ir al final.",
)
args = parser.parse_args()
if args.max_files < 1 or args.test_timeout < 1:
parser.error("--max-files y --test-timeout deben ser positivos")
test_command = args.test or []
if test_command and test_command[0] == "--":
test_command = test_command[1:]
report = {
"schema_version": 1,
"base": args.base,
"checks": [],
"changed_files": [],
"status": "failed",
}
def record(name: str, passed: bool, details: dict) -> None:
report["checks"].append(
{"name": name, "passed": passed, "details": details}
)
base_result = git("rev-parse", "--verify", args.base)
record("base_revision", base_result["exit_code"] == 0, base_result)
if base_result["exit_code"] != 0:
report["reason"] = "No se pudo resolver la revisión base."
print(json.dumps(report, indent=2, ensure_ascii=False))
return 1
diff = git("diff", "--name-only", "-z", f"{args.base}...HEAD")
if diff["exit_code"] != 0:
record("changed_files", False, diff)
print(json.dumps(report, indent=2, ensure_ascii=False))
return 1
changed_files = [name for name in diff["stdout"].split("\0") if name]
report["changed_files"] = changed_files
record("nonempty_patch", bool(changed_files), {"count": len(changed_files)})
record(
"file_count_limit",
len(changed_files) <= args.max_files,
{"actual": len(changed_files), "maximum": args.max_files},
)
if args.allow_prefix:
prefixes = [prefix.strip("/") for prefix in args.allow_prefix]
violations = [
name for name in changed_files
if not any(name == prefix or name.startswith(prefix + "/")
for prefix in prefixes)
]
record("allowed_paths", not violations, {"violations": violations})
diff_check = git("diff", "--check", f"{args.base}...HEAD")
record("git_diff_check", diff_check["exit_code"] == 0, diff_check)
if not test_command:
record("tests", False, {"error": "No se proporcionó --test."})
else:
result = run_command(test_command, timeout=args.test_timeout)
record(
"tests",
result["exit_code"] == 0 and not result["timed_out"],
result,
)
passed = all(check["passed"] for check in report["checks"])
report["status"] = "passed" if passed else "failed"
print(json.dumps(report, indent=2, ensure_ascii=False))
return 0 if passed else 1
if __name__ == "__main__":
sys.exit(main())
L'elenco degli argomenti e shell=False impediscono che il comando di test venga interpretato come un'espressione di shell. Questo non rende il processo sicuro: l'eseguibile e il codice sottoposto a test continuano a essere eseguiti con i permessi dell'ambiente. L'output viene limitato per evitare che un processo troppo verboso faccia crescere indefinitamente il rapporto.
Eseguire i controlli
Per prima cosa, conferma la patch in un branch e assicurati che la revisione di base sia disponibile localmente. Dalla radice del repository, per un progetto Python puoi eseguire:
python proofgate.py \
--base origin/main \
--allow-prefix src \
--allow-prefix tests \
--max-files 20 \
--test-timeout 300 \
--test python -m pytest -q
In questo esempio, gli ambiti consentiti sono le directory src e tests; adatta questi prefissi alla struttura effettiva del progetto. Per un altro linguaggio, modifica il comando dopo --test, ad esempio usando go test ./.... Il processo restituisce il codice 0 solo se tutti i controlli vengono superati; restituisce 1 in caso di errore o se non viene fornito un comando di test.
Il confronto usa base...HEAD, che rappresenta le modifiche del branch rispetto al suo antenato comune con la base. Se la revisione indicata non è disponibile, la barriera si interrompe prima di eseguire i test. Il rapporto distingue i controlli per nome, invece di ridurli tutti a un punteggio opaco.
Per verificare il comportamento senza dipendere da una specifica esecuzione, questo piccolo programma controlla che il rapporto JSON abbia stato approvato e che ogni controllo sia contrassegnato come superato. Salva il codice come check_report.py; riceve come argomento il file JSON generato reindirizzando l'output della barriera.
#!/usr/bin/env python3
"""Valida la struttura básica de un informe de proofgate."""
import json
import sys
from pathlib import Path
def main() -> int:
if len(sys.argv) != 2:
print("Uso: python check_report.py informe.json", file=sys.stderr)
return 2
try:
report = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
print(f"No se pudo leer el informe: {error}", file=sys.stderr)
return 1
checks = report.get("checks")
valid = (
report.get("schema_version") == 1
and isinstance(checks, list)
and bool(checks)
and report.get("status") == "passed"
and all(check.get("passed") is True for check in checks)
)
print("Informe aprobado" if valid else "Informe rechazado")
return 0 if valid else 1
if __name__ == "__main__":
raise SystemExit(main())
Il programma consente un semplice controllo della struttura e dello stato; non sostituisce la validazione dei test né certifica che il rapporto provenga da un'esecuzione affidabile. Per salvare e controllare un'esecuzione in sequenza:
python proofgate.py \
--base origin/main \
--allow-prefix src \
--allow-prefix tests \
--test python -m pytest -q > verification.json
python check_report.py verification.json
Trasformarlo in una regola CI
Una barriera è più coerente se viene eseguita a ogni modifica proposta. Questo flusso di GitHub Actions è un modello: occorre adattare i percorsi e l'installazione delle dipendenze al repository. La cronologia completa consente a Git di confrontare il branch con la base.
name: Verificación del parche
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Obtener el repositorio
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configurar Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Instalar dependencias de pruebas
run: python -m pip install pytest
- name: Verificar el parche
run: >-
python proofgate.py
--base origin/main
--allow-prefix src
--allow-prefix tests
--max-files 20
--test python -m pytest -q
Il controllo CI deve bloccare l'integrazione quando una verifica obbligatoria fallisce, non trasformare l'errore in un avviso. Nei progetti con politiche per la catena di approvvigionamento, anche le azioni di terze parti devono essere fissate secondo tali politiche; l'esempio usa tag di versione e non impone una strategia di pinning.
Cosa copre e cosa resta escluso
Git rileva alcuni problemi di formattazione con diff --check; il limite al numero di file e l'elenco dei prefissi applicano regole di ambito; l'esecutore registra il codice di uscita e il timeout. Nessuno di questi risultati dimostra che i test siano adeguati o che il comportamento soddisfi tutti i requisiti. OWASP SAMM raccomanda di verificare le regole importanti indipendentemente dalle istruzioni fornite all'assistente, preferibilmente tramite controlli deterministici come analisi statica, test o policy automatizzate.
Anche la verifica stessa può essere manipolata: per esempio, modificando un test affinché smetta di controllare il comportamento previsto. Proteggi i file di test, i flussi CI e lo script con una revisione adeguata, e aggiungi test di regressione che esprimano il contratto da una prospettiva indipendente dall'implementazione. La barriera proposta non rileva da sola tutti i modi in cui un test può essere indebolito.
Neppure il limite di tempo è una sandbox. Eseguire codice generato può consentire l'accesso a file, credenziali o rete, a seconda dei permessi dell'ambiente. Non fornire segreti di produzione a processi che eseguono modifiche non attendibili; usa un ambiente isolato e limita permessi e risorse.
OpenAI distingue tra l'ispezione delle tracce di un'esecuzione per eseguire il debug del comportamento di un agente e l'uso di insiemi di dati ed esecuzioni di valutazione quando è necessaria la ripetibilità. In questa barriera, il rapporto JSON svolge una funzione più circoscritta: documenta i controlli configurati per una specifica patch. Un passo successivo consiste nel salvare insieme al rapporto le revisioni esatte, il comando di test e le versioni degli strumenti.
Il risultato è volutamente modesto: una proposta viene approvata perché i controlli definiti sono stati eseguiti e hanno prodotto evidenze, non perché l'agente ha dichiarato di aver terminato. Prima dell'integrazione, resta una domanda concreta per il team: i controlli configurati coprono il comportamento che questa patch avrebbe dovuto preservare?
Fonti: DEV Community, OpenAI Developers, OWASP SAMM, arXiv