# Frailen for developers

> Three pieces: the tools, built from your OpenAPI; the identity, signed by your backend; and the chat, mounted in your product with one tag.

If you're a coding agent: read this page, then follow the prompts in [/agentes.md](/agentes.md). Prepare the integration on a branch; never approve, publish or deploy.

## The tools come from your OpenAPI

The importer builds one tool card per operation. Your team reviews its damage and read-back and approves it; nothing runs without that.

- **Only a GET can be a read.** Everything else is a write and goes through the brake.
- **Damage shows even when the route doesn't say so.** An amount hidden in the body flags money; an action on a whole collection flags bulk.
- **An undo is suggested, never assumed.** Until your team confirms it, the write is treated as if it can't be undone.
- **What isn't known stays a question.** A report says what was missing from your OpenAPI for each tool.

Damage flags: `dinero` (moves money), `masivo` (touches many at once), `terceros` (reaches third parties), `sin_reversa` (can't be undone). Patterns: `consultar`, `cambiar`, `pausar`, `agendar`, `cobrar`, `avisar`, `borrar`, `reportar`. The card format keeps its keys in Spanish.

```yaml
# herramientas/facturas.yaml
reembolsar_factura:
  descripcion: Refunds a paid invoice, in full or in part
  metodo: POST
  ruta: /v1/invoices/{factura_id}/refunds
  parametros:
    type: object
    properties:
      factura_id: { type: string }
      monto: { type: number }
      motivo: { type: string }
    required: [factura_id, monto]
  patron: cobrar
  se_deshace_con: ~                     # it can't be undone
  dano: [dinero, sin_reversa]           # dinero, masivo, terceros, sin_reversa
  relee_con:
    herramienta: ver_factura
    compara: [estado, reembolsado]
```

## Your backend signs the identity

A session signed with an asymmetric key and, in your API, a route that exchanges it for a short-lived token for that person (RFC 8693).

- **An ES256 session and your JWKS.** Frailen only keeps the public key. A shared secret won't do: with it, Frailen could sign for anyone.
- **`POST /frailen/token`.** Returns a token for that person, valid for 300 seconds or less. When a proposal is approved, one that works only for that order.
- **Signed receipts, if you want them.** Your API signs what it received on every write, and Frailen compares it with what it sent.

```http
POST /frailen/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<the session your backend signed>
&subject_token_type=urn:ietf:params:oauth:token-type:jwt
&requested_token_type=urn:ietf:params:oauth:token-type:access_token

HTTP/1.1 200 OK
{ "access_token": "…", "token_type": "Bearer", "expires_in": 300,
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token" }
```

## The chat mounts in your product

It runs in an iframe served by Frailen: neither your page nor a third-party script inside it can touch the confirmation of an action. The server signs each confirmation at the moment of the click: it works once, for 60 seconds, and only with a real click.

One tag:

```html
<script src="https://chat.frailen.com/chat/0.2.0/frailen.js"
  integrity="sha384-…" crossorigin="anonymous"
  data-empresa="acme-billing" data-asercion="/frailen/asercion"
  data-nombre="Assistant" data-nombre-empresa="Acme Billing"></script>
```

With code:

```js
import { montar } from "@frailen/chat-web"

const chat = montar({
  empresa: "acme-billing",
  marco: "https://chat.frailen.com",
  asercion: "/frailen/asercion",       // your backend signs it (ES256, 60 s)
  alEvento: (e) => e.tipo === "escritura" && releer(e.id),
})

// What the person has open: the agent uses it as a hint.
chat.contexto({ ruta: location.pathname, objeto: { tipo: "factura", id: "F-2291" } })
```

React:

```tsx
import { FrailenChat } from "@frailen/chat-web/react"

<FrailenChat
  empresa="acme-billing"
  marco="https://chat.frailen.com"
  asercion="/frailen/asercion"
  contexto={{ ruta: pathname, objeto }}
  alEvento={(e) => releer(e)}
/>
```

`@frailen/chat-web` isn't published to a public registry yet.

## Tests before turning it on

You write your cases with their rule before running them. The test bench grades them by reading your API back, not by reading what the agent replied.

- **The rule comes first.** A case with no written rule, or one that compares nothing, counts as a failure.
- **The state is what's graded.** What ended up in your product, read with different calls than the agent's.
- **Staging first.** Nothing reaches your customers until the bench passes and someone turns it on with their passkey.

```yaml
# pruebas/mover_vencimiento.yaml
id: mover_vencimiento
escrito: 2026-10-28          # the rule is written before it runs
persona: maria
mensajes:
  - Move invoice F-2291's due date to November 30
espera:                      # graded by reading back, not by reading the reply
  - herramienta: ver_factura
    argumentos: { factura_id: F-2291 }
    campos: { vence: "2026-11-30" }
regla: Passes if F-2291 is due on November 30 and F-2290 doesn't change.
```

## From your repository (soon)

A CLI and an MCP so you or your coding agent can integrate Frailen on a branch, with a draft pull request. They're being built and aren't available yet.

- **Staging only.** The CLI token lasts 8 hours and can't approve, lower the damage or touch production.
- **Your spec stays put.** The CLI trims it on your machine and uploads only the skeleton of what the agent can touch.

```bash
npx @frailen/cli login
npx @frailen/cli fuente recortar --agente soporte
npx @frailen/cli fuente registrar --entorno staging --en-seco
npx @frailen/cli banco
```

## More

- Build with agents (prompts): [/agentes.md](/agentes.md) · Security: [/seguridad.md](/seguridad.md)
- This page in Spanish: [/es/desarrolladores.md](/es/desarrolladores.md)
