Writ
Infraestructura de permisos para agentes de IA: compuertas de aprobación para cada escritura que realice tu agente, con un registro de auditoría a prueba de manipulaciones.
Documentación
pywrit
Encuentra las escrituras peligrosas en el código de tu agente Python y luego protégelo. pywrit es el
cliente Python y writ la CLI para Writ: una puerta de permiso/denegación que se sitúa
delante de las escrituras consecuentes de tu agente (base de datos, HTTP, archivos, correo electrónico, colas,
AWS) y registra un recibo encadenado por hash para cada decisión.

writ scan es local, determinista y gratuito: analiza tu código con el módulo
ast de Python, no realiza llamadas de red y no necesita clave API.
Instalación
pip install pywrit
Requiere Python 3.9+. Instala el comando writ y el cliente Python pywrit.
Desarrolladores de JavaScript/TypeScript: ejecuta el escáner sin configuración de Python vía npm:
npx writ-scan .
El paquete writ-scan envuelve el mismo escáner (incluyendo soporte TS/JS). En
la primera ejecución instala pywrit[polyglot] desde PyPI usando tu Python 3.9+
(una sola vez); después arranca al instante. Consulta npm/ para detalles,
anulaciones de entorno (WRIT_PYTHON, WRIT_SCAN_NO_INSTALL) y la
prueba de instalación.
Inicio rápido en 60 segundos: escanear → aplicar → proteger
1. Escanea. Ve qué funciones escriben y cuántas de ellas están protegidas.
writ scan .
writ scan: /path/to/support-agent
files scanned: 4 skipped: 0
write sites: 4 in 3 function(s)
gated: 0/4 (0%)
verbs discovered: 3
crm.update
payments.refund
tickets.close
También imprime un informe de riesgo (0-100, ponderado por nivel de riesgo), escribe los
verbos descubiertos en writ-policy.json y muestra la instrumentación que añadiría como un
diff unificado. Nada en tu código cambia todavía. writ scan . --score imprime solo el informe de riesgo.
2. Aplica. Inserta una puerta en la parte superior de cada función de escritura.
writ scan . --apply # shows the diff, then asks before writing
writ scan . --apply --yes # no prompt (e.g. in CI)
Cada función protegida ahora pregunta a Writ antes de escribir y falla de forma cerrada:
def issue_refund(charge_id, amount_cents):
if _writ_check("payments.refund") != "ALLOW":
raise PermissionError("writ denied payments.refund")
...
Vuelve a ejecutar writ scan . y verás gated: 4/4 (100%).
3. Protege. Obtén una clave API gratuita, carga la política descubierta y ejecuta tu agente.
writ key --email you@example.com # free API key + tenant
export WRIT_API_KEY=writ_... # read by the inserted gate
export WRIT_SPONSOR=acme WRIT_AGENT=support-agent # optional: who is acting
writ scan . --push-policy --key "$WRIT_API_KEY" # upload the discovered verb policy
Cada escritura protegida ahora recibe ALLOW, DENY o STEP_UP (un patrocinador humano debe
aprobar), y cada decisión se escribe en el registro de auditoría a prueba de manipulaciones de tu tenant:
writ receipts --key "$WRIT_API_KEY" # latest receipts
writ verify-chain --key "$WRIT_API_KEY" # verify the receipt hash chain
writ stream --key "$WRIT_API_KEY" # tail decisions live
writ report --key "$WRIT_API_KEY" # Agent Action Report
Qué detecta writ scan
Archivos Python (.py), analizados con el ast de la biblioteca estándar (sin dependencias adicionales):
| Categoría | Ejemplos |
|---|---|
| Base de datos | cursor.execute(...) / executemany / executescript (solo SQL de escritura; SELECT/WITH/... omitidos), session.add / commit / delete / merge / flush |
| HTTP | requests.post / put / patch, client.delete(...), session.request(...) |
| Archivos | open(..., "w"/"a"/"x"/"+"), Path.write_text / write_bytes / unlink / rename, os.remove / rename / makedirs, shutil.rmtree / move / copy |
| Correo electrónico | sendmail, send_message, send_email |
| Colas | publish, produce, enqueue, queue.send(...) |
| AWS SDK | put_object, put_item, delete_item, upload_file, send_message, start_execution, ... |
- Cada escritura se asigna a un verbo como
payments.refundocrm.update, inferido de la ruta del archivo y el nombre de la función. - Una función que ya llama a
writ_check(...)/_writ_check(...)cuenta como protegida. - Omitidos: pruebas, directorios ocultos, virtualenvs,
node_modules,dist,build. Usa--exclude SUBSTR(repetible) para omitir más. - No cubierto (revisar a mano): escrituras detrás de SQL construido dinámicamente, llamadas a SDK de terceros
como
stripe.Refund.create(...), colas de tareas diferidas o clientes compartidos varios niveles más abajo. El informe de riesgo lista estas brechas cada vez.
TypeScript / JavaScript (solo escaneo)
writ scan también encuentra sitios de escritura en TypeScript y JavaScript. Necesita el
extra opcional (basado en tree-sitter):
pip install 'pywrit[polyglot]'
writ scan .
Sin el extra, el escáner imprime una pista de una línea y continúa — el escaneo de Python nunca lo necesita.
TS/JS es solo escaneo: los hallazgos se listan con verbos para tu archivo de política,
pero writ scan --apply nunca reescribe archivos TS/JS — protégelos a mano.
Patrones cubiertos: escrituras fetch/axios, escrituras fs, SQL a través de clientes
estilo knex, escrituras de Prisma y llamadas a SDK de JS como stripe.refunds.create(...).
(Todavía no cubierto: los equivalentes de SDK Python como stripe.Refund.create(...)
— consulta "No cubierto" arriba.)

Acción de GitHub
Escanea cada solicitud de extracción en busca de sitios de escritura sin proteger. Los hallazgos aparecen como
anotaciones de verificación en el archivo y línea exactos, más un informe de riesgo en Markdown en el
resumen del trabajo. La verificación falla cuando un hallazgo sin proteger cumple tu
umbral de fail-on-risk (predeterminado: high).
- uses: actions/checkout@v4
- uses: AvenueDAdmin/pywrit@v0
with:
fail-on-risk: high # high | medium | low | never
No se necesita clave API. Referencia completa: docs/github-action.md · flujo de trabajo de ejemplo: examples/github-action/writ-scan.yml
Cliente Python
from pywrit import WritClient
client = WritClient(api_key="writ_...")
result = client.check(
sponsor_id="acme",
agent_id="agent-7",
verb="db.write",
target="prod.customers",
purpose="backfill region field",
)
if result.decision == "ALLOW":
# result.auth_token is a short-lived token bound to this exact write
perform_write(...)
elif result.decision == "STEP_UP":
# a human sponsor must approve first: client.grant(...), then re-check
...
else:
# DENY
...
¿Aún sin clave API? Prueba el sandbox sin clave:
client = WritClient()
client.sandbox({
"sponsorId": "acme",
"agentId": "agent-7",
"verb": "demo_write", # sandbox only allows demo_write ...
"target": "demo-customers", # ... on targets starting with demo-
"purpose": "trying the gate",
})
Lo que está cubierto:
check(...): la puerta.ALLOW/DENY/STEP_UP, más un recibo cada vezverify_token(...): valida un token de autenticaciónALLOW(detecta desviación de propósito)grant(...): aprobación de patrocinador humano para la rutaSTEP_UPget_policy()/set_policy(...): gestiona la política del tenantrevoke(...)/reinstate(...)/revoked(): el interruptor de apagadoreceipts()/receipt(id)/verify_chain()/stream_receipts(): el registro de auditoríasandbox(...): prueba sin clave, no se requiere clave API
Referencia de CLI
writ check --key writ_... --sponsor acme --agent agent-7 \
--verb db.write --target prod.customers --purpose "backfill region field"
writ policy --key writ_... --set payments.refund require_grant
writ revoke --sponsor acme --agent agent-7 --reason "runaway loop" # kill switch (sponsor token)
writ grant --sponsor acme --agent agent-7 --verb payments.refund \
--target ch_123 --purpose "approved refund" # STEP_UP approval (sponsor token)
Los comandos de token de patrocinador (revoke, reinstate, revoked, grant) leen
--sponsor-token o WRIT_SPONSOR_TOKEN. Ejecuta writ --help para la lista completa de comandos.
Insignia de README
Muestra que las escrituras de tu agente están protegidas por Writ. Dos variantes:
Estática (funciona hoy, sin clave API) — mismo diseño, sin recuento en vivo:
[](https://withwrit.com)
Dinámica (recuento en vivo de escrituras protegidas de 30 días desde tu registro de auditoría) — acuña un token de insignia y luego incrusta el fragmento que devuelve la API:
curl -s https://api.withwrit.com/v1/badge/tokens \
-H "Authorization: Bearer writ_..." \
-H "Content-Type: application/json" \
-d '{}'
El endpoint dinámico se envía con la puerta — hasta que esté en vivo, POST /v1/badge/tokens
devuelve 404 y la insignia estática de arriba es la que debes usar.
Consulta badge/ para documentación de incrustación y el modelo de verificación (qué
prueba la insignia — y qué no).
Documentación
Documentación completa: docs.withwrit.com · Inicio rápido: docs.withwrit.com/quickstart · Sitio: withwrit.com
Licencia
MIT