Skip to content
ARP / SPEC
VERSION v0.1 — DRAFT

Framework adapters

Audience: developers building an adapter so their agent framework can speak ARP without hand-rolling DIDComm + PDP plumbing.

Today, adapters are bundled inside @kybernesis/arp-cloud-bridge rather than published as separate packages. That package ships two:

  • kyberbot — first-class integration with KyberBot, uses typed /api/arp/* endpoints in KyberBot's server for data-layer policy enforcement.
  • generic-http — fallback for any agent that exposes an HTTP endpoint. The adapter forwards the ARP request as JSON to your endpoint and returns whatever you reply with.

Both are picked at arpc init time:

arpc init --framework kyberbot      # uses the kyberbot adapter
arpc init --framework generic-http  # uses the generic-http adapter

If neither fits, you author a new adapter against the contract below.

What an adapter actually does

The bridge process (arpc service install runs it) handles all the protocol-level work: DIDComm envelope encoding, PDP gating, audit logging, obligation application, queue-while-disconnected, key rotation. It then needs a handler to dispatch the decrypted, allowed action to the agent's local logic.

That handler is the adapter.

┌─ Peer agent ─┐    ┌─ Gateway ─┐    ┌─ Bridge (arpc) ──────────┐    ┌─ Agent ──┐
│              │ ─→ │  PDP +    │ ─→ │  decrypted, allowed       │ ─→ │          │
│              │    │  audit    │    │  action + obligations     │    │          │
│              │ ←─ │           │ ←─ │  egress: apply obligations │ ←─ │          │
└──────────────┘    └───────────┘    │  to response               │    └──────────┘
                                      │      ▲                     │
                                      │      └── adapter           │
                                      └────────────────────────────┘

The adapter is a small in-process module the bridge loads. It receives (action, params, context) and returns a response. Everything before and after happens in the bridge.

Adapter contract

An adapter exports one shape:

export interface ArpAdapter {
  name: string;              // matches what users pass to `arpc init --framework <name>`

  // Called when the bridge has an inbound action to dispatch.
  // The action has already been authorized by the PDP and obligations are attached.
  dispatch(input: AdapterInput): Promise<AdapterResponse>;

  // Optional: called once at bridge startup so the adapter can validate config.
  init?(opts: { agentDir: string; arpJson: ArpJson }): Promise<void>;

  // Optional: called when the bridge is shutting down so the adapter can clean up.
  shutdown?(): Promise<void>;
}

export interface AdapterInput {
  action: string;            // e.g. "messaging.relay.to_principal" or "files.project.files.read"
  params: Record<string, unknown>;
  context: {
    connection_id: string;
    source_did: string;       // the peer agent's DID
    obligations: Obligation[];
    audit_id: string;          // ties bridge logs to audit chain
  };
}

export interface AdapterResponse {
  ok: boolean;
  data?: unknown;             // returned to peer (still subject to egress obligations)
  error?: { code: string; message: string };
}

The bridge does the rest: applies redact_fields_except, rate_limit, max_size_mb, and any other obligations to data before re-encoding into a DIDComm /response envelope and sending it back to the peer.

What the adapter must NOT do

  • Do not call the PDP yourself. The bridge already gated the action; if it's in your dispatch, it's allowed. Re-checking is redundant and risks divergence.
  • Do not encrypt or sign envelopes. The bridge owns the wire format.
  • Do not write to the audit chain directly. Return ok: false with an error code if something goes wrong; the bridge writes the deny row.
  • Do not bypass obligations. Always return raw data; the bridge applies obligations on the way out. If your agent's data layer pre-filters (like KyberBot does for project_id), that's fine — it's an additional narrowing, not a replacement.

Where adapter code lives

Today: packages/arp-cloud-bridge/src/adapters/<name>.ts.

packages/arp-cloud-bridge/
├── src/
│   ├── adapters/
│   │   ├── kyberbot.ts        # POSTs typed actions to KyberBot's /api/arp/* endpoints
│   │   ├── generic-http.ts    # POSTs the action JSON to a configurable endpoint
│   │   └── index.ts           # adapter registry
│   └── bridge.ts
└── package.json

If you want to upstream a new framework adapter, open a PR adding it under src/adapters/ and registering it in index.ts. It's bundled with the published package; users get it on the next npm i -g @kybernesis/arp@latest.

Out-of-tree adapters

If you don't want to upstream, the bridge supports a customAdapterPath in arp.json:

{
  "framework": "custom",
  "customAdapterPath": "./my-adapter.js"
}

The bridge will require() that file at startup; export the ArpAdapter interface as the module default.

Worked example — generic-http

The simplest adapter, ~50 lines:

import type { ArpAdapter, AdapterInput, AdapterResponse } from '@kybernesis/arp-cloud-bridge';

export const genericHttpAdapter: ArpAdapter = {
  name: 'generic-http',

  async init({ arpJson }) {
    if (!arpJson.adapter?.endpoint) {
      throw new Error('generic-http adapter requires `adapter.endpoint` in arp.json');
    }
  },

  async dispatch(input: AdapterInput): Promise<AdapterResponse> {
    const endpoint = process.env.ARP_AGENT_ENDPOINT!;
    const res = await fetch(endpoint, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        action: input.action,
        params: input.params,
        connection_id: input.context.connection_id,
      }),
    });
    if (!res.ok) {
      return { ok: false, error: { code: 'agent_error', message: `agent returned ${res.status}` } };
    }
    return { ok: true, data: await res.json() };
  },
};

That's the whole shape. Real adapters add timeout handling, retries on idempotent actions, and richer error mapping.

Worked example — kyberbot

KyberBot ships typed /api/arp/* endpoints (one per action type), and the adapter dispatches by action name:

const ROUTE_MAP: Record<string, string> = {
  'messaging.relay.to_principal': '/api/arp/messaging/relay',
  'files.project.files.read':     '/api/arp/files/read',
  'files.project.files.summarize':'/api/arp/files/summarize',
  'tasks.list':                   '/api/arp/tasks/list',
  // …
};

The reason for typed routes: KyberBot enforces ARP policy at the data layer by appending WHERE project_id = ? AND classification IN (…) to the underlying query. The action name maps to a route that knows how to scope its query. See Memory & tagging for why this matters.

A generic-http adapter could call a single endpoint and let the agent figure out routing — but you lose the data-layer enforcement guarantee unless the agent re-implements it.

Roadmap (not shipped)

These are listed in the architecture as candidate adapters. None of them ship today. Anyone is welcome to author one against the contract above:

  • LangGraph
  • LangChain
  • CrewAI
  • Mastra
  • OpenClaw
  • AutoGen

The protocol is framework-agnostic. The work is mapping the framework's pre-action hook, post-action hook, and inbound-task surface to dispatch().

Conformance

@kybernesis/arp-testkit runs a fixture peer that exercises the full protocol: pair → request → deny → revoke → re-pair → audit-verify. To certify a new adapter, point the testkit at an example agent using your adapter and assert all checks pass.

import { runFullAudit } from '@kybernesis/arp-testkit';

const result = await runFullAudit({
  agentDid: 'did:web:test-agent.agent',
  bridgeEndpoint: 'http://localhost:8765',
});

expect(result.passed).toBe(result.total);
  • SDKs — package reference for everything an adapter imports
  • Architecture — system design overview
  • Spec — normative wire format the bridge implements