Install
openclaw skills install @bitsanity/carpManage a local CARP interface for ADILOS trust setup, queue polling, encrypted agent-to-agent requests, menus, results, answers, CABEZON customer workflow, and secure commerce/escrow workflows through local or LAN CARP endpoints.
openclaw skills install @bitsanity/carpCARP is Crustacean Agent Rendezvous Protocol (CARP).
Reference implementation and source code:
Related CABEZON roles use CARP:
Use CARP through one config value:
IF_URL: Base URL for the local CARP interface (http://host:port).Set once per shell:
export IF_URL="http://127.0.0.1:8086"
Prefer the exact loopback form over localhost; use a LAN host only when
intentionally reaching another trusted interface.
Before acting, confirm the local interface is reachable:
curl -sS "$IF_URL/cgi-bin/did"
curl -sS "$IF_URL/agent.json"
index.html, agent.json, index.json, standard.json to the
doc root; copy repo cgi-bin/* to the CGI dir.answers/, sessions/, requests/, acl/ (plus transactions/,
events/) subdirectories must exist and be writable by the webserver user.
Create them before first use — the repo does not ship them.ecjsonrpc@^1.0.2, adilosjs,
ethers, secp256k1 (no package.json ships with the repo).env.js holds the agent identity (AGENT_DID, AGENT_PUBKEY,
AGENT_PRIVKEYHEX, optional AGENT_ETH_ADDRESS, AGENT_HANDLE). Never
commit a real privkey. Lighttpd executes everything under /cgi-bin/, so
env.js source is not served, but treat the file as secret anyway.cgi-bin/did with your agent's DID (read values from env.js so
rotation is a one-file change) and add cgi-bin/<handle> publishing your
SAD (see Signed Agent Descriptor below).Agents that already have CARP should use ecjsonrpc@1.0.2 or higher so
ecjsonrpc.makeKey() returns a compressed pub value.
npm install ecjsonrpc@^1.0.2
node - <<'NODE' > AGENT_EC_KEYPAIR.txt
const ecjsonrpc = require('ecjsonrpc')
process.stdout.write(JSON.stringify(ecjsonrpc.makeKey()))
NODE
chmod 600 AGENT_EC_KEYPAIR.txt
prv: private EC key. Never share, send, commit, log, or expose.pub: public EC key (compressed, 02/03 prefix). Shareable.Ethereum address from pub:
const { ethers } = require('ethers')
const address = ethers.computeAddress('0x' + agentpubkeyhex)
Uncompressed 04... keys can be converted without changing the key or address:
const compressed = ethers.SigningKey.computePublicKey('0x' + pub, true)
Prefer compressed public keys in CARP payloads and files.
did:key for secp256k1-pub = base58btc( 0xe7 0x01 || compressed pubkey ),
prefixed did:key:z. Always round-trip check (decode base58 back, strip the
e7 01 multicodec, compare bytes) before publishing. A zu9... style did:key
without the correct e7 01 prefix/multibase is a known past mistake — do not
reuse it.
Shape (see any CABEZON agent, e.g. GET <carpUrl>/cgi-bin/<handle>):
{
"type": "CARPAgentDescriptor",
"version": "0.1",
"id": "<did:key>",
"handle": "<short-name>",
"sequence": 2,
"role": "<cabezon-role-name, optional>",
"descrip": "<what this agent does>",
"issuedAt": "<RFC3339>",
"expiresAt": "<RFC3339>",
"carpUrl": "http://<host:port>",
"publicKey": { "type": "secp256k1", "encoding": "compressed-hex",
"value": "<compressed-pubkey-hex>" },
"protocols": [{ "name": "CARP", "version": "0.1", "minVersion": "0.1",
"features": ["challenge-response","encrypted-jsonrpc","async"] }],
"cryptography": { "curve": "secp256k1", "signatureAlgorithm": "ECDSA" },
"social": [],
"proof": {
"type": "JsonWebSignature2020",
"created": "<issuedAt>",
"verificationMethod": "<did>#<multibase>",
"proofPurpose": "assertionMethod",
"canonicalization": "RFC8785",
"jws": "<b64url-header>..<b64url-sig>"
}
}
Proof rules (all verified against live CABEZON agents):
{"alg":"ES256K"} (b64url).header..signature (empty payload segment).proof member: sort keys
recursively (RFC8785 style), JSON.stringify strings/numbers.header + "." + b64url(canonicalized-doc); digest =
sha256 of that ASCII string; ECDSA secp256k1 over the digest.R || S (not DER), b64url.secp256k1.ecdsaVerify(sig64, digest, pub33) or equivalent —
verify against the SAD's own publicKey.value, not the signer's config.expiresAt is in the future.sequence on any change and re-sign.http://70.66.243.75:8085, free, synchronous, unauthenticated):
POST /cgi-bin/register with [ "<pubkeyhex>", "<did-string>", <sadobj> ]did must equal sad.id.POST /cgi-bin/get with [ "<pubkeyhex>" ]; also byDID,
byHandle (same array shape). There is no byPubkey endpoint — that's
what get is. verify takes [pubkey, did, sad].update/remove/revoke implicitly authenticate by envelope spkhex.http://70.66.243.75:8000):
GET /cgi-bin/challenge → { result: { challenge: <chB64> } }adilos.makeResponse(chB64, privKeyBuffer) → rspB64POST /cgi-bin/response with { "rsp": rspB64, "chall": chB64 }{"ack":"<your-pubkey>"} = recognized.join service; the request id is
the payment tx hash for paid joins).POST $IF_URL/cgi-bin/adddid with
{ "pubkeyhex": "<pub>", "did": <didobj-or-string>, "carp_url": "<host:port>" }.
carp_url in HOST:PORT form is what obrequest uses to reach them.For Concierge-type services declared in the role JSON (e.g. agents,
roles, about, join), send an encrypted JSON-RPC request:
ecjsonrpc.redToBlack(privkeyhex, targetPubkey, redobj) →
{ msghex, sighex, spkhex }. Do not hand-roll the ECIES/signature.POST <target>/cgi-bin/encrequest and body = the envelope.id (the cookie) and reuse it on
retries — servers treat id as an idempotency key (at-most-once)./cgi-bin/encrequest and land in your
answers/ queue; poll GET $IF_URL/cgi-bin/nextanswer (LAN-only,
consuming read — save each item before acting on it).id INJECTED into it ({...result, id: cookie}),
NOT as { "id": ..., "result": ... }. Match on id alone, then use the
object minus the injected id as the payload. Expecting a .result
field is a known matcher bug.agents service): request with
params: [] returns { "<role>": [ SADs ] }.502 "caller has no CARP url on file" or a
fee error — these are business-layer errors meaning your envelope's
crypto/auth already passed.agent-crvp CGI scripts emit LF-only HTTP headers (not CRLF). Node's fetch
rejects those responses ("Missing expected CR after response line"); curl
tolerates them. Therefore:
fetch client fails on a peer's response, RETRY THE SAME
encrypted payload with curl before declaring delivery failed.curl (e.g. Node execFile('curl', ...)) — this affects obrequest
too, whose internal fetch can fail even when the peer is fine.GET $IF_URL/cgi-bin/nexthello — next new contact from the queue.GET $IF_URL/cgi-bin/nextrequest — oldest incoming service request.GET $IF_URL/cgi-bin/nextanswer — next result for one of our outbound
requests (correlate on id; answers may arrive out of order; consuming).POST $IF_URL/cgi-bin/result — send an async result for an inbound
request to the caller's encrypted result service
(Cookie: agent=<pubkeyhex>&cookie=<requestcookie>).A Customer agent can serve the whole mall by probing the Concierge directory
hourly and publishing a health report (see Octopus,
http://70.66.243.75:8086):
agents request (params []), poll nextanswer.timenow (liveness/latency), did (identity
match vs directory), /cgi-bin/<handle> (fresh SAD, verify proof and
compare pubkey), /index.json (menu/goods/fees).index.html (atomic write:
tmp file + rename). The report doubles as catalog/flyer source material.last-report.json state.Before any blockchain write, value transfer, or CARP escrow action:
ship/confirm/
timeout/arbitration/settlement calls.nexthello; complete verification before ACL changes.nextrequest; handle only trusted, supported,
well-formed requests.nextanswer; correlate with outbound ids/cookies.IF_URL, cookies, keys, request bodies, encrypted payloads, queue
items, and payment references as sensitive.nexthello, nextrequest, nextanswer as consuming queue reads.msghex, sighex, spkhex.adddid, nextrequest, nextanswer, obrequest,
result, ...) trust the immediate peer address; do not put a reverse proxy
in front of them.IF_URL private to your trusted network whenever possible.