Documentación para desarrolladores

Construye con pap://

pap:// es un protocolo de delegación que puedes tener en las manos: firma un mandato que dice qué puede hacer un agente, ejecuta un handshake que prueba que ambas partes acordaron, y confina el trabajo a lo autorizado. Esta es la referencia de trabajo — cada tipo y llamada de abajo está en el código fuente.

Versión del workspace 0.8.3. Código abierto, licencia MIT.

Instalación

pap:// ofrece cuatro SDKs desde un solo árbol de código. Rust es la implementación de referencia; Python y TypeScript son enlaces sobre ella; WASM ejecuta el núcleo en el navegador. Elige tu lenguaje.

Los crates de Rust son la implementación de referencia. Clona el repo y ejecuta la suite de pruebas para confirmar tu toolchain, luego depende de los crates que necesites.

git clone https://gitlab.baursoftware.com/baur-software/pap.git
cd pap
cargo test

# run a worked example end to end
just run-example pap-delegation-chain-example

Del prompt al mandato

Lo que la gente no cree es que puedas escribir una frase y terminar con autoridad delegada. Esta es la respuesta honesta: la frase nunca se convierte en autoridad por sí sola. Se clasifica en una acción con nombre, y luego un humano firma un mandato para esa acción. Dos pasos separados — y el segundo es la clave de una persona, no la suposición de un modelo.

La clasificación corre en tres niveles, del más barato primero. Una URL sola es un atajo determinista — va directo a una acción de lectura sin ningún puntaje. Cualquier otra cosa se rankea con BM25, el mismo método de frecuencia de términos que usa un buscador, contra el catálogo de agentes disponibles: "reservar un hotel en París" puntúa contra un agente de alojamiento y se resuelve a una acción de reserva. Solo cuando BM25 no tiene una coincidencia confiable, el prompt pasa a un clasificador NLU para una lectura más considerada. Cada nivel es inspeccionable; ninguno es una caja negra que acuña poder.

Lo que sea que el pipeline resuelva es solo una propuesta. Se convierte en una sola ScopeAction, envuelta en un Scope, y el principal la firma. La firma es la concesión. Si el clasificador malinterpreta el prompt, el peor caso es un mandato con alcance hacia la acción equivocada — que el receptor aún puede rechazar, y que una delegación solo puede estrechar, nunca ampliar. El prompt es evidencia de intención; la clave del humano es lo que convierte la intención en autoridad.

use pap_core::mandate::Mandate;
use pap_core::scope::{DisclosureSet, Scope, ScopeAction};
use pap_did::PrincipalKeypair;

// 1. Classify the prompt into a schema.org action. Cheapest level first:
//      • bare URL           → "schema:ReadAction"     (deterministic)
//      • BM25 catalog match → e.g. "schema:ReserveAction"  (term-frequency rank)
//      • no confident match → NLU classifier ("schema:AnalyzeAction")
let prompt = "book a hotel in Paris";
let (resolved_action, _preferred_agent, _query) = route_intent(prompt, &catalog);
//   resolved_action == "schema:ReserveAction"

// 2. The resolved action is a PROPOSAL. It becomes one ScopeAction, and the
//    human principal signs the mandate. The signature is the grant of authority —
//    the scope is derived from classification, never hardcoded.
let principal = PrincipalKeypair::generate();
let mut mandate = Mandate::issue_root(
    principal.did(),
    orchestrator.did(),
    Scope::new(vec![ScopeAction::new(&resolved_action)]),
    DisclosureSet::empty(),
    ttl,
);
mandate.sign(principal.signing_key()).unwrap();

// A misread prompt can only produce a narrower, rejectable mandate —
// delegation can never widen scope past what was signed.

La acción resuelta se convierte en el alcance del mandato, luego el principal la firma — adaptado de examples/intent-routing. El alcance se deriva de la clasificación, nunca está fijo en el código.

El prompt propone. La firma concede. Un prompt mal clasificado no puede ampliar el alcance más allá de lo que se firmó — solo puede producir un mandato más estrecho y rechazable.

Mandatos y alcance

Un mandato es la concesión firmada de autoridad: quién delega, a quién, para qué acciones, con qué divulgaciones y hasta cuándo. El principal emite un mandato raíz y lo firma. Cualquier titular puede delegar uno más estrecho a un subagente.

La delegación no puede ampliarse. El alcance del hijo debe ser un subconjunto del padre, y el TTL del hijo no puede durar más que el del padre. Ambos se verifican al llamar a delegate, no solo se asumen: un alcance demasiado amplio devuelve DelegationExceedsScope, y un TTL más largo devuelve DelegationExceedsTtl.

Un Scope es un conjunto de ScopeActions. Scope::permits responde "¿se permite esta acción?"; Scope::contains responde "¿es todo este alcance un subconjunto de aquel?" — la misma verificación de subconjunto que aplica la delegación. MandateChain::verify_chain recorre toda la jerarquía, confirmando cada firma, cada vínculo con el padre y la contención de alcance en cada nivel.

use chrono::{Duration, Utc};
use pap_core::error::PapError;
use pap_core::mandate::{Mandate, MandateChain};
use pap_core::scope::{DisclosureSet, Scope, ScopeAction};
use pap_did::PrincipalKeypair;

// The human principal is the root of trust.
let principal = PrincipalKeypair::generate();
let orchestrator = PrincipalKeypair::generate();

// A root mandate: broad scope, 4-hour TTL.
let root_scope = Scope::new(vec![
    ScopeAction::new("schema:SearchAction"),
    ScopeAction::with_object("schema:ReserveAction", "schema:Flight"),
]);
let mut root = Mandate::issue_root(
    principal.did(),
    orchestrator.did(),
    root_scope,
    DisclosureSet::empty(),
    Utc::now() + Duration::hours(4),
);
root.sign(principal.signing_key()).unwrap();

// Delegate a strict subset to a sub-agent. The child scope must be a
// subset of the parent, and its TTL cannot outlast it.
let planner = PrincipalKeypair::generate();
let planner_scope = Scope::new(vec![ScopeAction::new("schema:SearchAction")]);
let mut child = root
    .delegate(planner.did(), planner_scope, DisclosureSet::empty(),
              Utc::now() + Duration::hours(3))
    .unwrap();
child.sign(orchestrator.signing_key()).unwrap();

// Widening is refused, not trusted.
let over_broad = Scope::new(vec![ScopeAction::new("schema:PayAction")]);
match child.delegate(planner.did(), over_broad, DisclosureSet::empty(),
                     Utc::now() + Duration::hours(2)) {
    Err(PapError::DelegationExceedsScope) => { /* rejected */ }
    _ => unreachable!(),
}

// Verify the whole chain: signatures, parent links, scope containment.
let chain = MandateChain { mandates: vec![root, child] };
chain.verify_chain(&[principal.verifying_key(), orchestrator.verifying_key()]).unwrap();

Una cadena de delegación de cuatro niveles de examples/delegation-chain. El alcance se estrecha y el TTL se acorta en cada salto; la cadena se verifica de extremo a extremo.

Los nombres de los métodos varían por lenguaje: Rust usa issue_root / verify; Python usa issue_root / verify_with_keypair con DisclosureSet.empty(); TypeScript usa Mandate.issueRoot en camelCase.

El handshake de seis fases

Una transacción entre dos agentes es un handshake de seis fases, dirigido por AgentClient contra el AgentServer del receptor. Usa run_full_handshake para sesiones estándar: ordena las fases en la secuencia requerida y hace obligatoria la Fase 5 — el recibo cofirmado —, de modo que una sesión no pueda saltarse en silencio su registro de responsabilidad.

El TTL del mandato se verifica antes de la E/S de red de cada fase, y cualquier error de protocolo cierra la sesión antes de devolver. Los métodos de fase de bajo nivel — present_token, exchange_did, send_disclosures, request_execution, exchange_receipt, close_session — están para streaming o recuperación personalizada, pero hacen fácil omitir la Fase 5, así que prefiere la llamada de alto nivel.

Del lado receptor, implementa el trait AgentHandler y sírvelo con AgentServer. El recorrido animado de las seis fases está en la página del protocolo. Ve el handshake animado →

use pap_transport::client::AgentClient;

let client = AgentClient::new("https://receiver.example");

// The recommended high-level call: it sequences all six phases in order
// and makes Phase 5 (the co-signed receipt) mandatory.
let (receipt, result) = client
    .run_full_handshake(
        token,                    // signed CapabilityToken authorising this interaction
        initiator_session_did,    // ephemeral: pap_did::SessionKeypair::generate()
        vec![],                   // SD-JWT disclosures — empty for a zero-disclosure session
        pre_signed_receipt,       // TransactionReceipt half-signed by the initiator
        mandate_expires_at,       // TTL, re-checked before every phase
    )
    .await?;

// `receipt` is co-signed by both parties; `result` is the Schema.org JSON-LD output.

run_full_handshake ordena las seis fases y devuelve el recibo cofirmado con el resultado en Schema.org.

Sobres y mensajes

Cada mensaje del protocolo viaja dentro de un Envelope que lo vincula a una sesión, un remitente, un destinatario y un número de secuencia. Tras la fase de intercambio de DID, cada sobre lleva una firma Ed25519 de la clave de sesión del remitente.

La firma cubre los bytes canónicos firmables — SHA-256 de session_id, secuencia y carga útil —, así que manipular cualquiera de los tres la invalida. Cambia la carga útil, cambia la secuencia o firma con la clave equivocada, y verify lo rechaza. Así se detectan el replay y la falsificación en la capa de transporte.

use pap_did::SessionKeypair;
use pap_proto::{Envelope, ProtocolMessage};

let initiator = SessionKeypair::generate();

let mut env = Envelope::new(
    "session-demo-001",
    initiator.did(),
    receiver.did(),
    1,                              // sequence number — replay protection
    ProtocolMessage::DisclosureOffer { disclosures: vec![] },
);

// Sign over the canonical bytes: SHA-256(session_id || sequence || payload).
env.sign(initiator.signing_key());
env.verify(&initiator.verifying_key()).unwrap();      // VALID

// Any tampering invalidates the signature.
let mut tampered = env.clone();
tampered.sequence = 999;
assert!(tampered.verify(&initiator.verifying_key()).is_err());   // REJECTED

Firmar un sobre y detectar manipulación, de examples/protocol-envelope.

Ejecución aislada

Envuelve cualquier AgentHandler en SandboxedHandlerWrapper para ejecutar la acción aprobada bajo confinamiento a nivel del sistema operativo. Una CapabilityPolicy define los límites — tiempo máximo de ejecución, y si se permite acceso a red, disco o subprocesos. Todo está denegado por defecto.

Qué aplica realmente esos límites depende de dónde ejecutes. El recibo nunca afirma una protección que no corrió: el AttestationReceipt registra un EnforcementBackend, y su CapabilityProof reporta lo que se aplicó de verdad, nunca lo que la política pidió.

use std::sync::Arc;
use pap_sandbox::{CapabilityPolicy, SandboxedHandlerWrapper, new_spawner};

// Everything is denied by default. Grant only what the action needs.
let policy = CapabilityPolicy {
    execution_timeout_secs: 30,
    network_allowed: false,
    filesystem_allowed: false,
    subprocess_allowed: false,
    ..Default::default()
};

let wrapped = SandboxedHandlerWrapper::new(
    inner_handler,          // Arc<dyn AgentHandler>
    new_spawner(),          // picks the enforcement backend for this OS
    policy,
    true,                   // sandbox_enabled
    agent_did,
    agent_name,
    "schema:SearchAction",  // action_type
);

// After a run, the co-signed AttestationReceipt reports the EnforcementBackend
// that actually ran and, in its CapabilityProof, exactly what was applied —
// never what the policy merely requested.

LinuxSeccomp

Se carga un filtro seccomp-BPF en el hilo de ejecución antes de que corra el agente. Deniega las llamadas al sistema de red y de subprocesos en el kernel — esos bloqueos son reales. seccomp filtra por número de llamada, no por punteros, así que no puede restringir por ruta de archivo; el recibo nunca afirma filesystem_restricted vía seccomp.

Docker

La acción corre en un contenedor hermano con capacidades retiradas, sin red, con disco de solo lectura y un tope de memoria. El runtime del contenedor aplica eso en el kernel, así que este backend es el que de verdad restringe el sistema de archivos.

NoneNative

macOS, BSD y Windows aún no tienen aplicación de llamadas al sistema conectada: obtienes un proceso separado y un tiempo límite, y cada bandera de restricción de llamadas en el recibo es false. Ejecuta bajo Docker (o un host Linux con seccomp) para aislamiento aplicado por el kernel.

Ve la frontera de ejecución en movimiento →

Ejemplos y lecturas

El repositorio incluye ejemplos ejecutables para cada parte del protocolo. Clónalo y ejecuta uno con just — por ejemplo la cadena de delegación o el sobre de protocolo de arriba.

delegation-chain

Una jerarquía de mandatos de cuatro niveles: estrechamiento de alcance, acortamiento de TTL, verificación de cadena, transiciones de estado de decaimiento y delegaciones demasiado amplias rechazadas.

protocol-envelope

Firma de sobres, detección de manipulación, rechazo de clave equivocada y prevención de replay por número de secuencia.

selective-disclosure-decay

Divulgación selectiva SD-JWT combinada con decaimiento de mandato — liberar solo los campos que una tarea necesita, luego degradar al no renovar.

travel-booking

Un flujo realista de reserva multiagente: varios agentes con alcance acotado transando entre servicios sin compartir el itinerario completo.

Lectura más profunda: la especificación completa del protocolo.

Una nota sobre LangChain, CrewAI y MCP

Existe una guía de integración que muestra pap:// acoplado a estos frameworks, y su código es correcto. Lo mantenemos honesto contándote lo que encontramos al intentarlo: estos stacks no pueden comportarse de verdad con pap://, y la razón es arquitectónica, no una función que falte.

Se construyeron alrededor de un solo contexto compartido que cada agente del grafo puede ver, con un proveedor de modelo alojado como raíz de confianza. pap:// existe para eliminar exactamente esas dos cosas. Una verificación de mandato puesta frente a una llamada del framework puede rechazar la llamada, pero no puede des-compartir el contexto que el framework ya entregó al modelo, ni puede confinar la ejecución que el framework corre en el mismo proceso. Obtienes una firma en la puerta y una planta abierta detrás — medio modelo de confianza, que es justo lo que pap:// se construyó para terminar.

Así que nuestra recomendación es no envolver estos frameworks y darlo por hecho. Construye directamente sobre pap:// — emite el mandato, ejecuta el handshake, aísla la ejecución — para que ambas fronteras sean reales. La guía de integración está ahí si tienes código existente que puentear durante una migración, pero trátala como una transición, no un destino.

Hecho por Baur Software. Licencia MIT.