clawdentity 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +22161 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +22156 -0
- package/dist/postinstall.d.ts +2 -0
- package/dist/postinstall.js +358 -0
- package/package.json +44 -0
- package/postinstall.mjs +48 -0
- package/skill-bundle/AGENTS.md +12 -0
- package/skill-bundle/openclaw-skill/dist/relay-to-peer.mjs +256 -0
- package/skill-bundle/openclaw-skill/skill/SKILL.md +170 -0
- package/skill-bundle/openclaw-skill/skill/references/clawdentity-protocol.md +175 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// src/transforms/peers-config.ts
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
var CLAWDENTITY_DIR = ".clawdentity";
|
|
6
|
+
var PEERS_FILENAME = "peers.json";
|
|
7
|
+
var PEER_ALIAS_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null;
|
|
10
|
+
}
|
|
11
|
+
function getErrorCode(error) {
|
|
12
|
+
if (!isRecord(error)) {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
return typeof error.code === "string" ? error.code : void 0;
|
|
16
|
+
}
|
|
17
|
+
function parseNonEmptyString(value, label) {
|
|
18
|
+
if (typeof value !== "string") {
|
|
19
|
+
throw new Error(`${label} must be a string`);
|
|
20
|
+
}
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if (trimmed.length === 0) {
|
|
23
|
+
throw new Error(`${label} must not be empty`);
|
|
24
|
+
}
|
|
25
|
+
return trimmed;
|
|
26
|
+
}
|
|
27
|
+
function parsePeerAlias(value) {
|
|
28
|
+
const alias = parseNonEmptyString(value, "peer alias");
|
|
29
|
+
if (alias.length > 128) {
|
|
30
|
+
throw new Error("peer alias must be at most 128 characters");
|
|
31
|
+
}
|
|
32
|
+
if (!PEER_ALIAS_PATTERN.test(alias)) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"peer alias must use only letters, numbers, dot, underscore, or hyphen"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return alias;
|
|
38
|
+
}
|
|
39
|
+
function parseDid(value) {
|
|
40
|
+
const did = parseNonEmptyString(value, "did");
|
|
41
|
+
if (!did.startsWith("did:")) {
|
|
42
|
+
throw new Error("did must start with 'did:'");
|
|
43
|
+
}
|
|
44
|
+
return did;
|
|
45
|
+
}
|
|
46
|
+
function parseProxyUrl(value) {
|
|
47
|
+
const candidate = parseNonEmptyString(value, "proxyUrl");
|
|
48
|
+
try {
|
|
49
|
+
return new URL(candidate).toString();
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error("proxyUrl must be a valid URL");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function parsePeerName(value) {
|
|
55
|
+
if (value === void 0) {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
return parseNonEmptyString(value, "name");
|
|
59
|
+
}
|
|
60
|
+
function parsePeerEntry(value) {
|
|
61
|
+
if (!isRecord(value)) {
|
|
62
|
+
throw new Error("peer entry must be an object");
|
|
63
|
+
}
|
|
64
|
+
const did = parseDid(value.did);
|
|
65
|
+
const proxyUrl = parseProxyUrl(value.proxyUrl);
|
|
66
|
+
const name = parsePeerName(value.name);
|
|
67
|
+
if (name === void 0) {
|
|
68
|
+
return { did, proxyUrl };
|
|
69
|
+
}
|
|
70
|
+
return { did, proxyUrl, name };
|
|
71
|
+
}
|
|
72
|
+
function parsePeersConfig(value, source) {
|
|
73
|
+
if (!isRecord(value)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Peer config validation failed at ${source}: root must be an object`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const peersRaw = value.peers;
|
|
79
|
+
if (peersRaw === void 0) {
|
|
80
|
+
return { peers: {} };
|
|
81
|
+
}
|
|
82
|
+
if (!isRecord(peersRaw)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Peer config validation failed at ${source}: peers must be an object`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const peers = {};
|
|
88
|
+
for (const [alias, peerValue] of Object.entries(peersRaw)) {
|
|
89
|
+
const normalizedAlias = parsePeerAlias(alias);
|
|
90
|
+
try {
|
|
91
|
+
peers[normalizedAlias] = parsePeerEntry(peerValue);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Peer config validation failed at ${source}: peers.${normalizedAlias}: ${reason}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { peers };
|
|
100
|
+
}
|
|
101
|
+
function resolvePeersConfigPath(options = {}) {
|
|
102
|
+
if (typeof options.configPath === "string" && options.configPath.trim().length > 0) {
|
|
103
|
+
return options.configPath.trim();
|
|
104
|
+
}
|
|
105
|
+
if (typeof options.configDir === "string" && options.configDir.trim().length > 0) {
|
|
106
|
+
return join(options.configDir.trim(), PEERS_FILENAME);
|
|
107
|
+
}
|
|
108
|
+
const home = typeof options.homeDir === "string" && options.homeDir.trim().length > 0 ? options.homeDir.trim() : homedir();
|
|
109
|
+
return join(home, CLAWDENTITY_DIR, PEERS_FILENAME);
|
|
110
|
+
}
|
|
111
|
+
async function loadPeersConfig(options = {}) {
|
|
112
|
+
const configPath = resolvePeersConfigPath(options);
|
|
113
|
+
let rawJson;
|
|
114
|
+
try {
|
|
115
|
+
rawJson = await readFile(configPath, "utf8");
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (getErrorCode(error) === "ENOENT") {
|
|
118
|
+
return { peers: {} };
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(rawJson);
|
|
125
|
+
} catch {
|
|
126
|
+
throw new Error(`Peer config at ${configPath} is not valid JSON`);
|
|
127
|
+
}
|
|
128
|
+
return parsePeersConfig(parsed, configPath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/transforms/relay-to-peer.ts
|
|
132
|
+
var DEFAULT_CONNECTOR_BASE_URL = "http://127.0.0.1:19400";
|
|
133
|
+
var DEFAULT_CONNECTOR_OUTBOUND_PATH = "/v1/outbound";
|
|
134
|
+
function isRecord2(value) {
|
|
135
|
+
return typeof value === "object" && value !== null;
|
|
136
|
+
}
|
|
137
|
+
function parseRequiredString(value) {
|
|
138
|
+
if (typeof value !== "string") {
|
|
139
|
+
throw new Error("Input value must be a string");
|
|
140
|
+
}
|
|
141
|
+
const trimmed = value.trim();
|
|
142
|
+
if (trimmed.length === 0) {
|
|
143
|
+
throw new Error("Input value must not be empty");
|
|
144
|
+
}
|
|
145
|
+
return trimmed;
|
|
146
|
+
}
|
|
147
|
+
function removePeerField(payload) {
|
|
148
|
+
const outbound = {};
|
|
149
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
150
|
+
if (key !== "peer") {
|
|
151
|
+
outbound[key] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return outbound;
|
|
155
|
+
}
|
|
156
|
+
function resolveRelayFetch(fetchImpl) {
|
|
157
|
+
const resolved = fetchImpl ?? globalThis.fetch;
|
|
158
|
+
if (typeof resolved !== "function") {
|
|
159
|
+
throw new Error("fetch implementation is required");
|
|
160
|
+
}
|
|
161
|
+
return resolved;
|
|
162
|
+
}
|
|
163
|
+
function parseConnectorBaseUrl(value) {
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = new URL(value);
|
|
167
|
+
} catch {
|
|
168
|
+
throw new Error("Connector base URL is invalid");
|
|
169
|
+
}
|
|
170
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
171
|
+
throw new Error("Connector base URL is invalid");
|
|
172
|
+
}
|
|
173
|
+
if (parsed.pathname === "/" && parsed.search.length === 0 && parsed.hash.length === 0) {
|
|
174
|
+
return parsed.origin;
|
|
175
|
+
}
|
|
176
|
+
return parsed.toString();
|
|
177
|
+
}
|
|
178
|
+
function normalizeConnectorPath(value) {
|
|
179
|
+
const trimmed = value.trim();
|
|
180
|
+
if (trimmed.length === 0) {
|
|
181
|
+
throw new Error("Connector outbound path is invalid");
|
|
182
|
+
}
|
|
183
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
184
|
+
}
|
|
185
|
+
function resolveConnectorEndpoint(options) {
|
|
186
|
+
const baseUrlInput = options.connectorBaseUrl ?? process.env.CLAWDENTITY_CONNECTOR_BASE_URL ?? DEFAULT_CONNECTOR_BASE_URL;
|
|
187
|
+
const pathInput = options.connectorPath ?? process.env.CLAWDENTITY_CONNECTOR_OUTBOUND_PATH ?? DEFAULT_CONNECTOR_OUTBOUND_PATH;
|
|
188
|
+
const baseUrl = parseConnectorBaseUrl(baseUrlInput.trim());
|
|
189
|
+
const path = normalizeConnectorPath(pathInput.trim());
|
|
190
|
+
return new URL(path, baseUrl).toString();
|
|
191
|
+
}
|
|
192
|
+
function mapConnectorFailure(status) {
|
|
193
|
+
if (status === 404) {
|
|
194
|
+
return new Error("Local connector outbound endpoint is unavailable");
|
|
195
|
+
}
|
|
196
|
+
if (status === 409) {
|
|
197
|
+
return new Error("Peer alias is not configured");
|
|
198
|
+
}
|
|
199
|
+
if (status === 400 || status === 422) {
|
|
200
|
+
return new Error("Local connector rejected outbound relay payload");
|
|
201
|
+
}
|
|
202
|
+
return new Error("Local connector outbound relay request failed");
|
|
203
|
+
}
|
|
204
|
+
async function postToConnector(endpoint, payload, fetchImpl) {
|
|
205
|
+
let response;
|
|
206
|
+
try {
|
|
207
|
+
response = await fetchImpl(endpoint, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": "application/json"
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify(payload)
|
|
213
|
+
});
|
|
214
|
+
} catch {
|
|
215
|
+
throw new Error("Local connector outbound relay request failed");
|
|
216
|
+
}
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
throw mapConnectorFailure(response.status);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function relayPayloadToPeer(payload, options = {}) {
|
|
222
|
+
if (!isRecord2(payload)) {
|
|
223
|
+
return payload;
|
|
224
|
+
}
|
|
225
|
+
const peerAliasValue = payload.peer;
|
|
226
|
+
if (peerAliasValue === void 0) {
|
|
227
|
+
return payload;
|
|
228
|
+
}
|
|
229
|
+
const peerAlias = parseRequiredString(peerAliasValue);
|
|
230
|
+
const peersConfig = await loadPeersConfig(options);
|
|
231
|
+
const peerEntry = peersConfig.peers[peerAlias];
|
|
232
|
+
if (!peerEntry) {
|
|
233
|
+
throw new Error("Peer alias is not configured");
|
|
234
|
+
}
|
|
235
|
+
const connectorEndpoint = resolveConnectorEndpoint(options);
|
|
236
|
+
const fetchImpl = resolveRelayFetch(options.fetchImpl);
|
|
237
|
+
const outboundPayload = removePeerField(payload);
|
|
238
|
+
await postToConnector(
|
|
239
|
+
connectorEndpoint,
|
|
240
|
+
{
|
|
241
|
+
peer: peerAlias,
|
|
242
|
+
peerDid: peerEntry.did,
|
|
243
|
+
peerProxyUrl: peerEntry.proxyUrl,
|
|
244
|
+
payload: outboundPayload
|
|
245
|
+
},
|
|
246
|
+
fetchImpl
|
|
247
|
+
);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
async function relayToPeer(ctx) {
|
|
251
|
+
return relayPayloadToPeer(ctx?.payload);
|
|
252
|
+
}
|
|
253
|
+
export {
|
|
254
|
+
relayToPeer as default,
|
|
255
|
+
relayPayloadToPeer
|
|
256
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clawdentity_openclaw_relay
|
|
3
|
+
description: This skill should be used when the user asks to "install clawdentity relay skill", "set up agent-to-agent relay from invite code", "connect OpenClaw agents with invite code", or needs OpenClaw peer communication through the local Clawdentity connector runtime.
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Clawdentity OpenClaw Relay Skill
|
|
8
|
+
|
|
9
|
+
This skill configures an OpenClaw agent to relay selected webhook payloads to a peer through the local Clawdentity connector runtime using a single invite code.
|
|
10
|
+
|
|
11
|
+
## Trigger Conditions
|
|
12
|
+
|
|
13
|
+
Use this skill when any of the following are requested:
|
|
14
|
+
- Install relay support for OpenClaw peer communication.
|
|
15
|
+
- Complete first-time setup from an invite code.
|
|
16
|
+
- Repair broken relay setup after config drift.
|
|
17
|
+
- Verify invite-code onboarding and peer mapping.
|
|
18
|
+
|
|
19
|
+
## Filesystem Truth (must be used exactly)
|
|
20
|
+
|
|
21
|
+
### OpenClaw state files
|
|
22
|
+
- OpenClaw state root (default): `~/.openclaw`
|
|
23
|
+
- OpenClaw config: `~/.openclaw/openclaw.json`
|
|
24
|
+
- Transform target path: `~/.openclaw/hooks/transforms/relay-to-peer.mjs`
|
|
25
|
+
- Workspace skill location: `~/.openclaw/workspace/skills/clawdentity-openclaw-relay/SKILL.md`
|
|
26
|
+
- Default transform source expected by CLI setup:
|
|
27
|
+
`~/.openclaw/workspace/skills/clawdentity-openclaw-relay/relay-to-peer.mjs`
|
|
28
|
+
|
|
29
|
+
### Clawdentity identity files
|
|
30
|
+
- Clawdentity root: `~/.clawdentity`
|
|
31
|
+
- Agent config: `~/.clawdentity/config.json`
|
|
32
|
+
- Agent identity directory: `~/.clawdentity/agents/<agent-name>/`
|
|
33
|
+
- Agent private key: `~/.clawdentity/agents/<agent-name>/secret.key`
|
|
34
|
+
- Agent AIT token: `~/.clawdentity/agents/<agent-name>/ait.jwt`
|
|
35
|
+
- Peer map: `~/.clawdentity/peers.json`
|
|
36
|
+
- Local selected agent marker: `~/.clawdentity/openclaw-agent-name`
|
|
37
|
+
- Relay runtime config: `~/.clawdentity/openclaw-relay.json`
|
|
38
|
+
|
|
39
|
+
## Invite Input Assumption
|
|
40
|
+
|
|
41
|
+
Provide a valid invite code string before running this skill.
|
|
42
|
+
Invite creation is outside this skill scope; this skill focuses on setup, pairing, and relay validation.
|
|
43
|
+
|
|
44
|
+
## Human + Agent Flow (strict user-style)
|
|
45
|
+
|
|
46
|
+
Follow this order. Do not skip steps.
|
|
47
|
+
|
|
48
|
+
## Tool Execution Contract (Agent)
|
|
49
|
+
|
|
50
|
+
This skill is operational. The agent must execute side effects via tools.
|
|
51
|
+
- Use the `exec` tool for npm/CLI/filesystem actions needed by onboarding.
|
|
52
|
+
- Do not ask the human to run shell commands that the agent can run itself.
|
|
53
|
+
- Ask the human only for missing secrets/inputs (for example API key or invite code).
|
|
54
|
+
- Report final status with concrete outputs (local DID, peer alias, written paths).
|
|
55
|
+
|
|
56
|
+
## CLI Command Utilization (required)
|
|
57
|
+
|
|
58
|
+
Use these commands as the default execution path for skill utilization:
|
|
59
|
+
|
|
60
|
+
- Initialize local CLI config:
|
|
61
|
+
- `clawdentity config init`
|
|
62
|
+
- Configure registry URL and API key when missing:
|
|
63
|
+
- `clawdentity config set registryUrl <registry-url>`
|
|
64
|
+
- `clawdentity config set apiKey <api-key>`
|
|
65
|
+
- Create and inspect local OpenClaw agent identity:
|
|
66
|
+
- `clawdentity agent create <agent-name> --framework openclaw`
|
|
67
|
+
- `clawdentity agent inspect <agent-name>`
|
|
68
|
+
- Apply OpenClaw invite setup:
|
|
69
|
+
- `clawdentity openclaw setup <agent-name> --invite-code <invite-code>`
|
|
70
|
+
- Start connector runtime for relay handoff:
|
|
71
|
+
- `clawdentity connector start <agent-name>`
|
|
72
|
+
- Optional persistent connector autostart:
|
|
73
|
+
- `clawdentity connector service install <agent-name>`
|
|
74
|
+
- Validate health and delivery:
|
|
75
|
+
- `clawdentity openclaw doctor`
|
|
76
|
+
- `clawdentity openclaw relay test --peer <alias>`
|
|
77
|
+
|
|
78
|
+
Pairing bootstrap for trust policy is API-based in the current release (no dedicated pairing CLI command yet):
|
|
79
|
+
|
|
80
|
+
- Owner/initiator starts pairing on initiator proxy:
|
|
81
|
+
- `POST /pair/start`
|
|
82
|
+
- Requires `Authorization: Claw <AIT>` and `x-claw-owner-pat`
|
|
83
|
+
- Body: `{"agentDid":"<responder-agent-did>"}`
|
|
84
|
+
- Responder confirms on responder proxy:
|
|
85
|
+
- `POST /pair/confirm`
|
|
86
|
+
- Requires `Authorization: Claw <AIT>`
|
|
87
|
+
- Body: `{"pairingCode":"<code-from-start>"}`
|
|
88
|
+
|
|
89
|
+
Successful confirm establishes mutual trust for the two agent DIDs. After confirm, both directions are allowed for trusted delivery.
|
|
90
|
+
|
|
91
|
+
1. Confirm prerequisites with the human.
|
|
92
|
+
- Confirm `clawdentity` CLI is installed and runnable.
|
|
93
|
+
- Confirm API key exists for this agent (if missing, ask the human for it).
|
|
94
|
+
- Confirm OpenClaw state directory path if non-default.
|
|
95
|
+
- Confirm OpenClaw base URL if local endpoint is non-default.
|
|
96
|
+
|
|
97
|
+
2. Confirm skill artifact exists in workspace skills directory.
|
|
98
|
+
- Ensure `~/.openclaw/workspace/skills/clawdentity-openclaw-relay/relay-to-peer.mjs` exists.
|
|
99
|
+
- If missing, install/update skill package contents before setup.
|
|
100
|
+
|
|
101
|
+
3. Configure local Clawdentity identity for this OpenClaw agent.
|
|
102
|
+
- Run `clawdentity config init`.
|
|
103
|
+
- If needed, ask the human for API key and run `clawdentity config set apiKey <key>`.
|
|
104
|
+
- Create identity: `clawdentity agent create <agent-name> --framework openclaw`.
|
|
105
|
+
- Verify identity: `clawdentity agent inspect <agent-name>`.
|
|
106
|
+
|
|
107
|
+
4. Ask the human for invite code.
|
|
108
|
+
- Prompt exactly for one invite code string.
|
|
109
|
+
- Do not ask for DID/proxy URL when invite code is present.
|
|
110
|
+
|
|
111
|
+
5. Run automated setup from invite code.
|
|
112
|
+
- Execute:
|
|
113
|
+
`clawdentity openclaw setup <agent-name> --invite-code <invite-code>`
|
|
114
|
+
- Use `--openclaw-dir <path>` when state directory is non-default.
|
|
115
|
+
- Use `--openclaw-base-url <url>` when local OpenClaw HTTP endpoint is non-default.
|
|
116
|
+
- Use `--peer-alias <alias>` only when alias override is required.
|
|
117
|
+
|
|
118
|
+
6. Verify setup outputs.
|
|
119
|
+
- Confirm setup reports:
|
|
120
|
+
- peer alias
|
|
121
|
+
- peer DID
|
|
122
|
+
- updated OpenClaw config path
|
|
123
|
+
- installed transform path
|
|
124
|
+
- OpenClaw base URL
|
|
125
|
+
- relay runtime config path
|
|
126
|
+
- Confirm `~/.clawdentity/openclaw-agent-name` is set to the local agent name.
|
|
127
|
+
|
|
128
|
+
7. Start connector runtime for local relay handoff.
|
|
129
|
+
- Run `clawdentity connector start <agent-name>`.
|
|
130
|
+
- Optional: run `clawdentity connector service install <agent-name>` for persistent autostart.
|
|
131
|
+
|
|
132
|
+
8. Complete trust pairing bootstrap.
|
|
133
|
+
- Run pairing start (`POST /pair/start`) from the owner/initiator side.
|
|
134
|
+
- Share returned one-time `pairingCode` with responder side.
|
|
135
|
+
- Run pairing confirm (`POST /pair/confirm`) from responder side.
|
|
136
|
+
- Confirm pairing success before relay test.
|
|
137
|
+
|
|
138
|
+
9. Validate with user-style relay test.
|
|
139
|
+
- Run `clawdentity openclaw doctor` to verify setup health and remediation hints.
|
|
140
|
+
- Run `clawdentity openclaw relay test --peer <alias>` to execute a probe.
|
|
141
|
+
- Confirm probe success and connector-mediated delivery logs.
|
|
142
|
+
- Human asks Alpha to send a real request with `peer: "beta"` and verifies peer delivery.
|
|
143
|
+
|
|
144
|
+
## Required question policy
|
|
145
|
+
|
|
146
|
+
Ask the human only when required inputs are missing:
|
|
147
|
+
- Missing Clawdentity API key.
|
|
148
|
+
- Unclear OpenClaw state directory.
|
|
149
|
+
- Non-default OpenClaw base URL.
|
|
150
|
+
- Missing invite code.
|
|
151
|
+
- Local connector runtime or peer network route is unknown or unreachable from agent runtime.
|
|
152
|
+
|
|
153
|
+
## Failure Handling
|
|
154
|
+
|
|
155
|
+
If setup or relay fails:
|
|
156
|
+
- Report precise missing file/path/value.
|
|
157
|
+
- Fix only the failing config/input.
|
|
158
|
+
- Ensure connector runtime is active (`clawdentity connector start <agent-name>`).
|
|
159
|
+
- Re-run `clawdentity openclaw doctor`.
|
|
160
|
+
- Re-run `clawdentity openclaw relay test --peer <alias>`.
|
|
161
|
+
- Re-run the same user-style flow from step 5 onward only after health checks pass.
|
|
162
|
+
|
|
163
|
+
## Bundled Resources
|
|
164
|
+
|
|
165
|
+
### References
|
|
166
|
+
| File | Purpose |
|
|
167
|
+
|------|---------|
|
|
168
|
+
| `references/clawdentity-protocol.md` | Invite format, peer map schema, connector handoff envelope, and runtime failure mapping |
|
|
169
|
+
|
|
170
|
+
Directive: read the reference file before troubleshooting relay contract or connector handoff failures.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Clawdentity Relay Protocol Reference
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Define the exact runtime contract used by `relay-to-peer.mjs`.
|
|
6
|
+
|
|
7
|
+
## Filesystem Paths
|
|
8
|
+
|
|
9
|
+
### OpenClaw files
|
|
10
|
+
- `~/.openclaw/openclaw.json`
|
|
11
|
+
- `~/.openclaw/hooks/transforms/relay-to-peer.mjs`
|
|
12
|
+
- `~/.openclaw/workspace/skills/clawdentity-openclaw-relay/SKILL.md`
|
|
13
|
+
|
|
14
|
+
### Clawdentity files
|
|
15
|
+
- `~/.clawdentity/config.json`
|
|
16
|
+
- `~/.clawdentity/agents/<agent-name>/secret.key`
|
|
17
|
+
- `~/.clawdentity/agents/<agent-name>/ait.jwt`
|
|
18
|
+
- `~/.clawdentity/peers.json`
|
|
19
|
+
- `~/.clawdentity/openclaw-agent-name`
|
|
20
|
+
- `~/.clawdentity/openclaw-relay.json`
|
|
21
|
+
|
|
22
|
+
## Invite Code Contract
|
|
23
|
+
|
|
24
|
+
Invite codes are prefixed with `clawd1_` and contain base64url JSON:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"v": 1,
|
|
29
|
+
"issuedAt": "2026-02-15T20:00:00.000Z",
|
|
30
|
+
"did": "did:claw:agent:01H...",
|
|
31
|
+
"proxyUrl": "https://beta-proxy.example.com/hooks/agent",
|
|
32
|
+
"alias": "beta",
|
|
33
|
+
"name": "Beta Agent"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Rules:
|
|
38
|
+
- `v` must be `1`.
|
|
39
|
+
- `issuedAt` is ISO-8601 UTC timestamp.
|
|
40
|
+
- `did` must be an agent DID.
|
|
41
|
+
- `proxyUrl` must be absolute `http` or `https`.
|
|
42
|
+
- `alias` is optional but preferred for zero-question setup.
|
|
43
|
+
|
|
44
|
+
## Peer Map Schema
|
|
45
|
+
|
|
46
|
+
`~/.clawdentity/peers.json` must be valid JSON:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"peers": {
|
|
51
|
+
"beta": {
|
|
52
|
+
"did": "did:claw:agent:01H...",
|
|
53
|
+
"proxyUrl": "https://beta-proxy.example.com/hooks/agent",
|
|
54
|
+
"name": "Beta Agent"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Rules:
|
|
61
|
+
- peer alias key uses `[a-zA-Z0-9._-]`
|
|
62
|
+
- `did` required and must begin with `did:`
|
|
63
|
+
- `proxyUrl` required and must be a valid absolute URL
|
|
64
|
+
- `name` optional
|
|
65
|
+
|
|
66
|
+
## Proxy Pairing Prerequisite
|
|
67
|
+
|
|
68
|
+
Relay delivery policy is trust-pair based on proxy side. Pairing must be completed before first cross-agent delivery.
|
|
69
|
+
|
|
70
|
+
Current pairing contract is API-based (no dedicated CLI pairing command):
|
|
71
|
+
|
|
72
|
+
1. Initiator owner starts pairing:
|
|
73
|
+
- `POST /pair/start`
|
|
74
|
+
- headers:
|
|
75
|
+
- `Authorization: Claw <AIT>`
|
|
76
|
+
- `x-claw-owner-pat: <owner-pat>`
|
|
77
|
+
- body:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"agentDid": "did:claw:agent:01RESPONDER..."
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
2. Responder confirms pairing:
|
|
86
|
+
- `POST /pair/confirm`
|
|
87
|
+
- headers:
|
|
88
|
+
- `Authorization: Claw <AIT>`
|
|
89
|
+
- body:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"pairingCode": "01PAIRCODE..."
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Rules:
|
|
98
|
+
- `pairingCode` is one-time and expires.
|
|
99
|
+
- Confirm establishes mutual trust for the initiator/responder pair.
|
|
100
|
+
- Same-agent sender/recipient is allowed by policy without explicit pair entry.
|
|
101
|
+
|
|
102
|
+
## Relay Input Contract
|
|
103
|
+
|
|
104
|
+
The OpenClaw transform reads `ctx.payload`.
|
|
105
|
+
|
|
106
|
+
- If `payload.peer` is absent:
|
|
107
|
+
- return payload unchanged
|
|
108
|
+
- do not relay
|
|
109
|
+
- If `payload.peer` exists:
|
|
110
|
+
- resolve peer from `peers.json`
|
|
111
|
+
- remove `peer` from forwarded body
|
|
112
|
+
- send JSON POST to local connector outbound endpoint
|
|
113
|
+
- return `null` to skip local handling
|
|
114
|
+
|
|
115
|
+
## Relay Agent Selection Contract
|
|
116
|
+
|
|
117
|
+
Relay resolves local agent name in this order:
|
|
118
|
+
1. transform option `agentName`
|
|
119
|
+
2. `CLAWDENTITY_AGENT_NAME`
|
|
120
|
+
3. `~/.clawdentity/openclaw-agent-name`
|
|
121
|
+
4. single local agent fallback from `~/.clawdentity/agents/`
|
|
122
|
+
|
|
123
|
+
## Local OpenClaw Base URL Contract
|
|
124
|
+
|
|
125
|
+
`~/.clawdentity/openclaw-relay.json` stores the OpenClaw upstream base URL used by local proxy runtime fallback:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"openclawBaseUrl": "http://127.0.0.1:18789",
|
|
130
|
+
"updatedAt": "2026-02-15T20:00:00.000Z"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Rules:
|
|
135
|
+
- `openclawBaseUrl` must be absolute `http` or `https`.
|
|
136
|
+
- `updatedAt` is ISO-8601 UTC timestamp.
|
|
137
|
+
- Proxy runtime precedence is: `OPENCLAW_BASE_URL` env first, then `openclaw-relay.json`, then built-in default.
|
|
138
|
+
|
|
139
|
+
## Connector Handoff Contract
|
|
140
|
+
|
|
141
|
+
The transform does not send directly to the peer proxy. It posts to the local connector runtime:
|
|
142
|
+
- Default endpoint: `http://127.0.0.1:19400/v1/outbound`
|
|
143
|
+
- Optional overrides:
|
|
144
|
+
- `CLAWDENTITY_CONNECTOR_BASE_URL`
|
|
145
|
+
- `CLAWDENTITY_CONNECTOR_OUTBOUND_PATH`
|
|
146
|
+
|
|
147
|
+
Outbound JSON body sent by transform:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"peer": "beta",
|
|
152
|
+
"peerDid": "did:claw:agent:01H...",
|
|
153
|
+
"peerProxyUrl": "https://beta-proxy.example.com/hooks/agent",
|
|
154
|
+
"payload": {
|
|
155
|
+
"event": "agent.message"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Rules:
|
|
161
|
+
- `payload.peer` is removed before creating the `payload` object above.
|
|
162
|
+
- Transform sends `Content-Type: application/json` only.
|
|
163
|
+
- Connector runtime is responsible for Clawdentity auth headers and request signing when calling peer proxy.
|
|
164
|
+
|
|
165
|
+
## Error Conditions
|
|
166
|
+
|
|
167
|
+
Relay fails when:
|
|
168
|
+
- no selected local agent can be resolved
|
|
169
|
+
- peer alias missing from config
|
|
170
|
+
- local connector outbound endpoint is unavailable (`404`)
|
|
171
|
+
- local connector reports unknown peer alias (`409`)
|
|
172
|
+
- local connector rejects payload (`400` or `422`)
|
|
173
|
+
- local connector outbound request fails (network/other non-2xx)
|
|
174
|
+
|
|
175
|
+
Error messages should include file/path context but never print secret content.
|