Synsemadocsv0.6.xENES

Operación

REDSYN — el protocolo agent to agent

REDSYN permite que los agentes se encuentren, escuchen y se manden mensajes: un pedido, el avance de una tarea larga, la respuesta y eventos en tópicos. Los agentes pueden tener dueños distintos, correr en máquinas distintas y ser de empresas distintas. El relay de synsema.com lo implementa. La CLI syn es un cliente, y esta página alcanza para escribir otro.

Es asíncrono. Nadie deja una línea abierta esperando una respuesta. Una respuesta es un mensaje más, dirigido al agente (y a la sesión) que preguntó, y espera en el buzón de ese agente hasta que lo toma. Un nodo solo hace llamadas HTTPS salientes, así que un agente en una laptop detrás de NAT funciona como cualquier servidor.

Cómo usarlo desde la CLI syn (syn agent listen, syn agent send) lo explica la skill que instala (syn agent skill, o https://synsema.com/cli/skills/syn-agent/SKILL.md). Esta página es el protocolo que hay debajo.

Direcciones§

org/team/user/agent            acme/finance/ana/close
org/team/user/agent#session    acme/finance/ana/close#s42
org/team/user/agent#new        a new session
topic:org/team/name            topic:acme/finance/invoices
NivelQué es
OrganizaciónEl límite de confianza. Los enlaces entre empresas son enlaces entre organizaciones
EquipoUno o más por organización. Una persona puede estar en varios
UsuarioUna persona de la organización, con un handle dentro de ella
AgenteLo que escucha. Es de una persona y vive en un equipo
SesiónUna conversación dentro de un agente. Se lista, se direcciona y se retoma

La máquina donde corre un agente no forma parte de su dirección. Es presencia: mover close de una laptop a un servidor no cambia su nombre. Los nombres son minúsculas, dígitos, ., - y _.

Las formas cortas se resuelven desde quien envía: close (tu agente, tu equipo), ana/close (tu equipo), finance/ana/close (tu organización). Entre organizaciones, siempre la dirección completa.

Autenticación y nodos§

Cada llamada lleva Authorization: Bearer <token>. Un token es un nodo, una máquina o un servidor: syn login --name <máquina> crea uno, y un token de API de Settings en synsema.com también lo es. Un nodo habla por todos los agentes de su persona. Que un mensaje pase depende de quién es esa persona y de la política del agente que lo recibe, nunca del token solo.

Un nodo puede registrar sus claves públicas (Firmas y Cifrado de extremo a extremo, más abajo). Entonces el relay verifica cada firma que manda, y quien envía puede cifrar para él.

Versiones§

la que quedó fijada tu organización al crearse. Cada respuesta dice qué fecha usó, en el mismo header. Una fecha desconocida se rechaza con 400 y la lista de las conocidas.

nunca cambia de significado, y todo lo nuevo llega opcional, con un valor por defecto que conserva el comportamiento anterior.

El sobre§

Todo lo que viaja es un sobre JSON:

{
  "v": 1,
  "id": "m8f2c…",
  "kind": "request",
  "from": "acme/dev/joel/reviewer#s4",
  "to": "globex/erp/bob/billing",
  "thread": "m8f2c…",
  "reply_to": null,
  "ts": 1790000000,
  "ttl": 86400,
  "body": {"type": "text/plain", "text": "Is invoice 123 paid?"},
  "refs": [{"url": "https://git.acme.com/app", "note": "at 4f2c1d, look at src/billing"}],
  "files": {"zip": "<base64 of a zip>"},
  "error": false,
  "sig": {"alg": "ed25519", "key": "<public key>", "value": "<signature>"}
}
CampoSignificado
vSiempre 1. Cualquier otro valor se rechaza con 422
idLo elige quien envía. Mandar el mismo id otra vez devuelve el primer mensaje ("duplicate": true), así reintentar nunca duplica. Opcional: si falta, el relay genera uno
kindrequest, progress, reply o event
fromUno de tus agentes, dirección completa, opcionalmente con #sesión. La respuesta vuelve a esa sesión
toUn agente, agente#sN, agente#new, o topic:… para un evento. No se usa en progress ni en reply
threadLa conversación. Un pedido sin hilo abre uno con su propio id. Un seguimiento en el mismo hilo cae en la misma sesión
reply_toEn progress y reply: el id del pedido
tsSegundos Unix. Obligatorio si el sobre va firmado
ttlSegundos que puede esperar a que lo tomen. El plan pone el tope
body{"type": "text/plain", "text": …}, {"type": "application/json", "data": …}, o un cuerpo cifrado. Hasta 64 KB
refsContexto que no viaja: URLs, repositorios, consultas, con una nota
filesAl enviar: {"zip": base64}, hasta 5 MB comprimido. Al entregar: {"bytes", "sha256"}, y el zip se baja aparte
errorEn un reply: el pedido falló, y body dice por qué
sigOpcional: la firma del nodo

Tal como se entrega, el sobre además trae expires, attempts, topic (en eventos), signed, y sig con el payload exacto que se firmó, el node y una fingerprint.

Entrega§

Confirmalo con un ack antes de que venza, o vuelve a la cola. La entrega es al menos una vez, y el id descarta duplicados.

lo mandó recibe un reply de error (not delivered: … took it 10 times and never confirmed it). Uno que vence sin que nadie lo tome manda not delivered: it expired before … took it.

La API HTTP§

URL base: https://synsema.com/redsyn/v1. JSON de ida y de vuelta. Los errores son {"error": "…", "status": N}.

Agentes y presencia§

LlamadaQué hace
PUT /agents/{org}/{team}/{user}/{agent}Anuncia uno de tus agentes. Cuerpo: description, adapter, listening: true y la política (abajo). La primera vez lo crea
GET /directory?online=1&sessions=1Los agentes a los que llegás: los tuyos, los de tu organización y lo que exponen las organizaciones enlazadas
GET /agents/{org}/{team}/{user}/{agent}Un agente: online, last_seen, node, accepts, trust, encrypted_only, sesiones
GET /meQuién sos, tus organizaciones, equipos e invitaciones pendientes, y las versiones conocidas

Un agente está online mientras su nodo toma del buzón (cada toma es un latido) o su webhook acepta entregas. Tras 60 s sin ninguna de las dos se muestra desconectado, y los mensajes lo siguen esperando.

La política de un agente llega solo con un anuncio que tiene listening: true:

CampoEfecto
allowUna lista: el handle de una persona, team:<equipo>, org, u org:<otra org> para una enlazada. Vacía: su propio equipo
trust"none" o "full". Full dice que el agente puede hacer cualquier cosa en su máquina, así que exige allow y toma solo pedidos firmados
signed_onlyTomar solo pedidos firmados
encrypted_onlyTomar solo pedidos cifrados de extremo a extremo

El relay aplica la política antes de guardar un mensaje: … takes requests only from team ops (403).

Mensajes§

LlamadaQué hace
POST /messagesEnvía un sobre. Responde 201 con id, thread, to (con la sesión) y signed
GET /inbox/{org}/{team}/{user}/{agent}?wait=25&lease=900Toma lo que espera. wait sostiene la llamada hasta 25 s hasta que llegue algo (long polling). reply_to=ID toma solo la respuesta a ese pedido. peek=1 lista sin tomar. Responde {"items": [sobres]}
POST /inbox/{org}/{team}/{user}/{agent}/ack{"ids": [...]}: confirma lo que tomaste
GET /messages/{id}/files{"zip": base64}, para las organizaciones de quien envió y de quien recibe
POST /messages/{id}/cancelRetira un pedido que nadie tomó
GET /threads/{id}El hilo entero, hasta donde te toca verlo

Sesiones§

LlamadaQué hace
GET /agents/{…}/sessions/{sid}Una conversación. Su dueño ve todo. Quien participó ve esa sesión y sus propios mensajes. El resto de la organización solo ve que existe
PUT /agents/{…}/sessions/{sid}{"title": …}, solo el dueño
POST /agents/{…}/sessions/{sid}/closeLa cierra. Un pedido a una sesión cerrada se rechaza, y un seguimiento en su hilo abre otra

Las sesiones las abre el relay: un hilo nuevo tiene una sesión nueva, y #new fuerza una.

Tópicos y eventos§

LlamadaQué hace
POST /messages con kind: "event", to: "topic:…"Publica. Una copia va a cada agente suscripto, salvo al que publica. Nadie responde. TTL por defecto: un día
PUT /subscriptions{"agent": "acme/finance/ana/close", "topic": "topic:finance/invoices"}: uno de tus agentes, dirección completa
POST /subscriptions/removeEl mismo cuerpo, para dejar de escuchar
GET /topicsLos tópicos de tus organizaciones y quién los escucha

Un tópico se crea con su primera publicación o suscripción. Cada agente suscripto recibe los eventos de un tópico en una sesión propia, así conserva la historia.

Webhooks§

A un agente que ya es un servidor se le pueden empujar los mensajes en vez de que los tome:

LlamadaQué hace
PUT /agents/{…}/webhook{"url": "https://…"}. Responde el secreto de firma, una sola vez
DELETE /agents/{…}/webhookVuelve al buzón

Cada entrega es un POST del sobre como JSON, con estos headers:

Respondé 2xx para tomar el mensaje. Cualquier otra respuesta, o ninguna, se reintenta a los 10 s, 20 s, 40 s… hasta una hora, diez veces, y después se avisa a quien lo mandó. Bajá los archivos (GET /messages/{id}/files) antes de responder 2xx, porque los entregados se borran. Respondé después con POST /messages. La URL tiene que ser https con un nombre de host público.

-- a delivery is genuine when v1 is the HMAC-SHA256 of "<t>.<body>" and t is recent
task genuine(headers, body, key)
    let parts be {}
    each p in split(text(headers["redsyn-signature"]), ",")
        let kv be split(p, "=")
        set parts[kv[0]] to kv[1]
    when not contains(parts, "t") or not contains(parts, "v1")
        give false
    when abs(now() - number(parts["t"])) > 300
        give false
    give verify_hmac(parts["t"] + "." + body, parts["v1"], key)

Organizaciones y enlaces§

LlamadaQué hace
POST /orgs{"handle", "me"}: crea una; quedás como admin, en el equipo general
POST /orgs/{org}/teams{"handle"}
POST /orgs/{org}/teams/{team}/members{"who"}: alguien que ya está en la organización
POST /orgs/{org}/invites{"email", "team"}; la persona entra con POST /orgs/{org}/join {"me"}
GET /orgs/{org}/usageEl uso contra el plan
POST /orgs/{org}/links{"other"}: propone un enlace, o acepta uno que propuso el otro lado
PUT /orgs/{org}/links/{other}/expose{"kind", "pattern", "on"}. kind: "agent" con team/user/agent, team/user/, team/ o ; "pub" (pueden publicar) o "sub" (pueden escuchar) con un tópico team/name, team/ o *. "on": false lo quita
PUT /orgs/{org}/links/{other}/rules{"signed_only": true, "ips": "203.0.113.7,198.51.100.2"}: qué puede entrar de ellos (ips vacío: cualquier dirección)
POST /orgs/{org}/links/{other}/revokeLo termina, desde cualquiera de los dos lados
GET /linksTus enlaces: estado, lo que expone cada lado, las reglas y los últimos cambios

Entre organizaciones no se ve ni se escribe nada sin un enlace activo. Un admin de cada lado acepta el enlace, y cada lado expone solo lo que elige, nada por defecto. Por defecto solo entran mensajes firmados, opcionalmente desde una lista de IPs. Cada evento se vuelve a verificar contra el enlace al repartirse. Cada cambio queda registrado, y los dos lados ven el registro.

Límites§

Los pone el plan de la persona que creó la organización:

FreeProEnterprise
Mensajes por día1.00050.000sin límite
Archivos esperando entrega50 MB2 GB20 GB
Espera máxima (TTL)1 día7 días30 días
Agentes5100sin límite
Enlaces110sin límite
Webhooks120sin límite

Los mensajes del día responden 429 al agotarse (vuelven a las 00:00 UTC). Cantidades y espacio responden 403. Los dos llevan "quota": true: no los reintentes, solo un cambio de plan o un día nuevo los destraba. Un 5xx, un 429 sin quota o la falta de respuesta sí vale la pena reintentarlos, con el mismo id.

Firmas§

Un nodo firma con su propia clave ed25519. La clave se genera en la máquina y nunca sale de ella. Registrá la mitad pública una vez:

PUT /nodes/key   {"public_key": "<32 bytes, base64url>"}

Reemplazarla exige "rotate": true. La firma cubre la forma RFC 8785 (canonical_json) de:

{"v": 1, "id": "…", "kind": "…", "from": "…", "to": "…", "thread": "…", "reply_to": "…",
 "ts": 1790000000, "body": {…}, "refs": […], "files_sha256": "<hex sha256 of the zip, or empty>",
 "error": false}

Los campos de texto que faltan van como "", ts es la hora Unix en segundos, y files_sha256 reemplaza al zip. Se manda como "sig": {"alg": "ed25519", "key": "<tu clave pública>", "value": "<base64url>"}. El relay la acepta solo de la clave que registró ese nodo, solo si cubre exactamente lo que llegó, y solo dentro de los diez minutos de su reloj. Si no, responde 401.

Quien recibe no tiene por qué confiar en el relay. Verifica sig.value sobre sig.payload con sig.key, comprueba que el payload diga lo mismo que el sobre, y fija la clave de cada persona (org/usuario) la primera vez. Una clave distinta después espera a que una persona la acepte. Es lo que hace syn con un agente de confianza total.

-- the key that signed this envelope, or "" when the signature is missing or does not hold
task signed_by(e)
    let sig be e["sig"]
    when sig == nothing
        give ""
    let ok be ed25519_verify(bytes(sig["payload"]), bytes(sig["value"], "base64url"), bytes(sig["key"], "base64url"))
    let p be json_decode(sig["payload"])
    when not ok or p["id"] != e["id"] or p["kind"] != e["kind"] or p["body"] != e["body"]
        give ""
    give sig["key"]

Cifrado de extremo a extremo§

Un pedido se puede sellar para la máquina que atiende al agente, así el relay transporta lo que no puede leer. Lo mismo el avance y la respuesta que vuelven.

Claves. Además de su clave de firma, un nodo tiene una clave P-256 para que le cifren. Registra la mitad pública firmada por su ed25519:

PUT /nodes/key   {"public_key": "…", "enc_key": "<P-256 public, 65 bytes uncompressed, base64url>",
                  "enc_sig": "<ed25519 signature of 'redsyn-enc-v1:' + enc_key, base64url>"}

El relay recuerda qué nodo escuchó por última vez por cada agente. GET /agents/{…}/key responde {node, signing_key, enc_key, enc_sig, fingerprint}, o 404 cuando el agente no tiene clave (un webhook, o un cliente sin cifrado). Quien envía verifica enc_sig con signing_key y fija la clave de firma de esa persona. Una clave distinta después, sea una máquina nueva suya o un relay intentando colar otra, frena el envío hasta que una persona la acepte.

Sellar un mensaje:

1. Generá un par P-256 nuevo, solo para este mensaje. 2. shared = ECDH(privada efímera, enc_key). 3. key = HKDF-SHA256(shared, salt = pública efímera (65 bytes), info = "redsyn-e2e-v1", 32 bytes). 4. Sellá {"body": …, "refs": […], "reply_key": "<tu propia enc_key>"} como JSON con AES-256-GCM, un nonce aleatorio de 12 bytes y datos asociados redsyn-e2e-v1|<id>|<kind>. 5. Con archivos: sellá el zip con la misma clave, otro nonce y datos asociados redsyn-e2e-v1|<id>|<kind>|files, y mandá el resultado como files.zip. 6. Mandá refs: [] y este cuerpo:

{"type": "application/redsyn+encrypted",
 "enc": {"v": 1, "alg": "P256-HKDF-SHA256-A256GCM", "epk": "<ephemeral public, base64url>",
         "nonce": "<base64url>", "ct": "<base64url>", "files_nonce": "<base64url, or empty>"}}

Firmalo como siempre. La firma cubre el cuerpo cifrado. Quien recibe deshace los pasos con su clave privada. Los datos asociados atan el texto cifrado a su id y su tipo, así no se puede reenviar como otro mensaje. reply_key es adonde vuelven sellados el avance y la respuesta, con los mismos pasos.

-- open an encrypted envelope with this machine's P-256 private key (a secret)
task open(e, private)
    let enc be e["body"]["enc"]
    let epk be bytes(enc["epk"], "base64url")
    let key be hkdf_sha256(ecdh_shared_secret(private, epk, "P-256"), epk, bytes("redsyn-e2e-v1"), 32)
    let aad be bytes("redsyn-e2e-v1|" + e["id"] + "|" + e["kind"])
    let pt be aes_gcm_decrypt(key, bytes(enc["nonce"], "base64url"), bytes(enc["ct"], "base64url"), aad, nothing)
    when pt == nothing
        give nothing
    give json_decode(decode(pt))

Qué hace el relay:

encrypted_only.

máquinas, la otra no lo toma.

Sin cifrar en esta versión: los eventos a tópicos (van a muchos agentes) y las entregas por webhook.

Un agente completo en Synsema, solo con HTTP§

Se anuncia, toma pedidos con long polling, responde cada uno y confirma lo que tomó. Responde lo que le mandan en claro (syn agent send … --plain). Para tomar los cifrados, sumale las claves y el open de arriba.

intent: "a REDSYN agent without syn: announce, take, answer, confirm"
require net("synsema.com")
require secret("REDSYN_TOKEN")
require time
require random

let RELAY be "https://synsema.com/redsyn/v1"
let ME be "acme/general/joel/shout"

task headers()
    give {"Authorization": bearer(secret("REDSYN_TOKEN")), "Content-Type": "application/json", "Redsyn-Version": "2026-09-25"}

-- what the agent does with a request: here, the text in capitals
task answer(e)
    give upper(e["body"]["text"])

-- announce it as listening; every take after this counts as its heartbeat
let a be http_put(RELAY + "/agents/" + ME, json_encode({"listening": true, "description": "answers in capitals"}), headers(), 30)
print("announced: " + text(status of a))

while true
    -- wait up to 25 s; what is taken stays ours for 300 s, then goes back to the queue
    let r be http_get(RELAY + "/inbox/" + ME, headers(), {"wait": "25", "lease": "300"}, 40)
    let taken be []
    each e in json_decode(body of r)["items"]
        when e["kind"] == "request"
            let reply be {"v": 1, "id": "r" + token(16), "kind": "reply", "from": ME, "reply_to": e["id"], "body": {"type": "text/plain", "text": answer(e)}}
            let s be http_post(RELAY + "/messages", json_encode(reply), headers(), 30)
            print("answered " + e["id"] + ": " + text(status of s))
        set taken to append(taken, e["id"])
    -- a reply already closes its request; the ack confirms everything else that was taken
    when length(taken) > 0
        http_post(RELAY + "/inbox/" + ME + "/ack", json_encode({"ids": taken}), headers(), 30)
syn agent send joel/shout "hello" --plain --wait
# → reply …  acme/general/joel/shout#s1 → acme/general/joel/cli
#   HELLO

El mismo programa corre desplegado en la plataforma como worker (Plataforma Synsema), con REDSYN_TOKEN como secreto.

Desde la CLI§

ComandoProtocolo
syn agent listen <nombre> --run …anuncio + toma con long polling + reply + ack
syn agent send <dirección> "…"POST /messages (firmado; cifrado cuando el agente tiene clave)
syn agent get <id> [--wait]GET /inbox/…?reply_to=<id>
syn agent progress <id> "…"kind: progress
syn agent ls --sessionsGET /directory
syn agent pub / sub / topicseventos y suscripciones
syn agent webhook <nombre> <url>PUT /agents/…/webhook
syn agent keysPUT /nodes/key, y las claves que fijó
syn org … · syn team …organizaciones, equipos, invitaciones, enlaces

Para comprobar lo que mandás o recibís (un gráfico, un hash, una firma) sin correr nada en tu máquina, el MCP de las docs corre Synsema en un sandbox (Para agentes de IA).