@perkos/perkos-a2a 0.8.13 → 0.8.15
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/README.md +53 -1
- package/dist/agent.js +118 -0
- package/dist/agent.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +2 -2
- package/dist/pairing.d.ts +56 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +66 -0
- package/dist/pairing.js.map +1 -0
- package/docs/nexus-communications-server.md +191 -0
- package/docs/pairing-registration.md +110 -0
- package/package.json +2 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type PairingRuntime = "openclaw" | "hermes" | "custom" | string;
|
|
2
|
+
export interface PairingAgentIdentity {
|
|
3
|
+
agentName: string;
|
|
4
|
+
agentId?: string;
|
|
5
|
+
publicKey: string;
|
|
6
|
+
privateKey: string;
|
|
7
|
+
keyType: "ed25519";
|
|
8
|
+
createdAt: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PairingClaimRequest {
|
|
11
|
+
agentName: string;
|
|
12
|
+
publicKey: string;
|
|
13
|
+
keyType: "ed25519";
|
|
14
|
+
runtime: PairingRuntime;
|
|
15
|
+
capabilities: string[];
|
|
16
|
+
transport: {
|
|
17
|
+
relay: true;
|
|
18
|
+
publicUrl?: string;
|
|
19
|
+
};
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
export interface PairingCredential {
|
|
23
|
+
agentId: string;
|
|
24
|
+
agentName: string;
|
|
25
|
+
system: string;
|
|
26
|
+
relayUrl: string;
|
|
27
|
+
relayApiKey: string;
|
|
28
|
+
scopes: string[];
|
|
29
|
+
approvedAt: string;
|
|
30
|
+
expiresAt?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface PairingClaimResponse {
|
|
33
|
+
status: "pending" | "approved" | "rejected";
|
|
34
|
+
requestId: string;
|
|
35
|
+
inviteId: string;
|
|
36
|
+
credential?: PairingCredential;
|
|
37
|
+
message?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface StoredPairingProfile {
|
|
40
|
+
identity: PairingAgentIdentity;
|
|
41
|
+
credential?: PairingCredential;
|
|
42
|
+
lastPairing?: PairingClaimResponse;
|
|
43
|
+
}
|
|
44
|
+
export declare function defaultPairingPath(agentName: string): string;
|
|
45
|
+
export declare function loadOrCreateIdentity(agentName: string, path?: string): PairingAgentIdentity;
|
|
46
|
+
export declare function savePairingProfile(profile: StoredPairingProfile, path?: string): void;
|
|
47
|
+
export declare function buildClaimRequest(input: {
|
|
48
|
+
identity: PairingAgentIdentity;
|
|
49
|
+
runtime: PairingRuntime;
|
|
50
|
+
capabilities?: string[];
|
|
51
|
+
publicUrl?: string;
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}): PairingClaimRequest;
|
|
54
|
+
export declare function claimUrlFromInvite(inviteUrl: string): string;
|
|
55
|
+
export declare function claimPairingInvite(inviteUrl: string, request: PairingClaimRequest): Promise<PairingClaimResponse>;
|
|
56
|
+
//# sourceMappingURL=pairing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEvE,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,cAAc,CAAC;IACxB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE;QACT,KAAK,EAAE,IAAI,CAAC;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,WAAW,CAAC,EAAE,oBAAoB,CAAC;CACpC;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG5D;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,SAAgC,GAAG,oBAAoB,CAiBlH;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,EAAE,IAAI,SAAiD,GAAG,IAAI,CAG7H;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE,cAAc,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GAAG,mBAAmB,CAUtB;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG5D;AAED,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkBvH"}
|
package/dist/pairing.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { generateKeyPairSync } from "crypto";
|
|
5
|
+
export function defaultPairingPath(agentName) {
|
|
6
|
+
const safeName = agentName.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
7
|
+
return join(homedir(), ".perkos", "a2a", "agents", `${safeName}.json`);
|
|
8
|
+
}
|
|
9
|
+
export function loadOrCreateIdentity(agentName, path = defaultPairingPath(agentName)) {
|
|
10
|
+
if (existsSync(path)) {
|
|
11
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
12
|
+
if (raw.identity?.agentName && raw.identity?.publicKey && raw.identity?.privateKey)
|
|
13
|
+
return raw.identity;
|
|
14
|
+
}
|
|
15
|
+
const pair = generateKeyPairSync("ed25519", {
|
|
16
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
17
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
agentName,
|
|
21
|
+
publicKey: pair.publicKey,
|
|
22
|
+
privateKey: pair.privateKey,
|
|
23
|
+
keyType: "ed25519",
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function savePairingProfile(profile, path = defaultPairingPath(profile.identity.agentName)) {
|
|
28
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
29
|
+
writeFileSync(path, `${JSON.stringify(profile, null, 2)}\n`, { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
export function buildClaimRequest(input) {
|
|
32
|
+
return {
|
|
33
|
+
agentName: input.identity.agentName,
|
|
34
|
+
publicKey: input.identity.publicKey,
|
|
35
|
+
keyType: input.identity.keyType,
|
|
36
|
+
runtime: input.runtime,
|
|
37
|
+
capabilities: input.capabilities?.length ? input.capabilities : ["chat", "tasks:receive", "messages:send"],
|
|
38
|
+
transport: { relay: true, publicUrl: input.publicUrl },
|
|
39
|
+
metadata: input.metadata,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function claimUrlFromInvite(inviteUrl) {
|
|
43
|
+
const trimmed = inviteUrl.replace(/\/+$/, "");
|
|
44
|
+
return `${trimmed}/claim`;
|
|
45
|
+
}
|
|
46
|
+
export async function claimPairingInvite(inviteUrl, request) {
|
|
47
|
+
const response = await fetch(claimUrlFromInvite(inviteUrl), {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "content-type": "application/json" },
|
|
50
|
+
body: JSON.stringify(request),
|
|
51
|
+
});
|
|
52
|
+
const body = await response.text();
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = body ? JSON.parse(body) : {};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error(`Pairing server returned non-JSON response (${response.status}): ${body}`);
|
|
59
|
+
}
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const message = typeof parsed === "object" && parsed && "error" in parsed ? String(parsed.error) : body;
|
|
62
|
+
throw new Error(`Pairing claim failed (${response.status}): ${message}`);
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=pairing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.js","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Nexus Communications Server Pattern
|
|
2
|
+
|
|
3
|
+
> Deployment note: the standalone communications/transport server source now lives in [`PerkOS-xyz/PerkOS-Transport`](https://github.com/PerkOS-xyz/PerkOS-Transport). This package remains the A2A protocol/plugin implementation used by clients and runtimes.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
This is the recommended PerkOS A2A topology for Nexus-style project rooms and agent swarms.
|
|
7
|
+
|
|
8
|
+
## Goals
|
|
9
|
+
|
|
10
|
+
- Keep user/runtime agents behind NAT with outbound-only WebSocket connections.
|
|
11
|
+
- Use the relay as a registrar/rendezvous server, not as an unrestricted public chat server.
|
|
12
|
+
- Let a product backend orchestrate project-room messages, task assignment, persistence, and auth.
|
|
13
|
+
- Allow OpenClaw/Hermes workers to receive A2A envelopes and call back into the backend with results.
|
|
14
|
+
|
|
15
|
+
## Components
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Nexus UI
|
|
19
|
+
-> Nexus Backend / communications server
|
|
20
|
+
-> PerkOS A2A orchestrator client
|
|
21
|
+
-> PerkOS A2A relay hub
|
|
22
|
+
-> OpenClaw or Hermes runtime worker
|
|
23
|
+
-> authenticated callback to Nexus Backend
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Relay hub
|
|
27
|
+
|
|
28
|
+
Run the relay on a stable host or locally for development:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
RELAY_PORT=6060 \
|
|
32
|
+
RELAY_API_KEYS=devkey \
|
|
33
|
+
a2a-relay
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For production, prefer `RELAY_AGENTS`/`registeredAgents` so every agent has an explicit approved registration key instead of a shared relay key.
|
|
37
|
+
|
|
38
|
+
### Backend / communications server
|
|
39
|
+
|
|
40
|
+
The backend owns durable product state. It should:
|
|
41
|
+
|
|
42
|
+
1. register or launch runtime agents and persist their stable `agentId`;
|
|
43
|
+
2. create an A2A client named after the backend/orchestrator;
|
|
44
|
+
3. dispatch structured envelopes to `agentId` targets;
|
|
45
|
+
4. receive authenticated runtime callbacks;
|
|
46
|
+
5. persist messages, task logs, artifacts, and status.
|
|
47
|
+
|
|
48
|
+
Envelope examples:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"kind": "nexus.agent_discussion",
|
|
53
|
+
"version": 1,
|
|
54
|
+
"walletAddress": "<wallet>",
|
|
55
|
+
"projectId": "<project>",
|
|
56
|
+
"projectName": "Demo Project",
|
|
57
|
+
"projectGoal": "Coordinate agents",
|
|
58
|
+
"topic": "What should we build first?",
|
|
59
|
+
"agentId": "<stable-agent-id>",
|
|
60
|
+
"agentName": "Builder Agent",
|
|
61
|
+
"agentRuntime": "OpenClaw",
|
|
62
|
+
"agentPlugins": ["github", "browser", "storage"]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"kind": "nexus.project_task",
|
|
69
|
+
"version": 1,
|
|
70
|
+
"walletAddress": "<wallet>",
|
|
71
|
+
"projectId": "<project>",
|
|
72
|
+
"taskId": "<task>",
|
|
73
|
+
"taskName": "Prepare first deliverable",
|
|
74
|
+
"taskPrompt": "Implement the first slice",
|
|
75
|
+
"agentId": "<stable-agent-id>",
|
|
76
|
+
"agentName": "Builder Agent",
|
|
77
|
+
"agentRuntime": "OpenClaw"
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Runtime worker
|
|
82
|
+
|
|
83
|
+
A runtime worker should:
|
|
84
|
+
|
|
85
|
+
- connect outbound to the relay using `NEXUS_A2A_RELAY_URL` and `NEXUS_A2A_RELAY_API_KEY`;
|
|
86
|
+
- register with `agentName = stable agentId` to avoid display-name collisions;
|
|
87
|
+
- validate envelope kind and target `agentId`;
|
|
88
|
+
- execute the requested work or discussion action;
|
|
89
|
+
- call back to the backend using a bearer token or equivalent service auth.
|
|
90
|
+
|
|
91
|
+
## Local smoke test
|
|
92
|
+
|
|
93
|
+
NexusApp includes a local smoke test that starts:
|
|
94
|
+
|
|
95
|
+
- fake Nexus backend callback server;
|
|
96
|
+
- embedded PerkOS A2A relay;
|
|
97
|
+
- real Nexus runtime worker process;
|
|
98
|
+
- A2A orchestrator client.
|
|
99
|
+
|
|
100
|
+
It verifies both `nexus.agent_discussion` and `nexus.project_task` envelopes reach the worker and produce backend callbacks:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd NexusApp/Backend
|
|
104
|
+
npm run smoke:a2a
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Expected output:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
ok - fake Nexus backend listening
|
|
111
|
+
ok - PerkOS A2A relay listening
|
|
112
|
+
ok - orchestrator discovered runtime worker over relay
|
|
113
|
+
ok - A2A discussion envelope reached worker and posted callback
|
|
114
|
+
ok - A2A project-task envelope reached worker and executed callback
|
|
115
|
+
ok - a2a local smoke passed
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Live PerkOS transport endpoint
|
|
119
|
+
|
|
120
|
+
Current app-server deployment:
|
|
121
|
+
|
|
122
|
+
- VPS: `62.238.28.49`
|
|
123
|
+
- Public secure endpoint: `wss://transport.perkos.xyz/a2a`
|
|
124
|
+
- TLS: Let's Encrypt via the existing Caddy proxy
|
|
125
|
+
- Container: `perkos-a2a-relay`
|
|
126
|
+
- Server path: `/opt/perkos-a2a-relay`
|
|
127
|
+
- Relay secret: stored only on the VPS in `/opt/perkos-a2a-relay/.env`
|
|
128
|
+
|
|
129
|
+
Use this from Nexus/worker env:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
NEXUS_A2A_ENABLED=true
|
|
133
|
+
NEXUS_A2A_RELAY_URL=wss://transport.perkos.xyz/a2a
|
|
134
|
+
NEXUS_A2A_RELAY_API_KEY=<relay-key-from-vps-env>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Docker deployment
|
|
138
|
+
|
|
139
|
+
The repo includes a minimal relay runner for VPS/app-server deployments:
|
|
140
|
+
|
|
141
|
+
- `deploy/Dockerfile`
|
|
142
|
+
- `deploy/relay-runner.mjs`
|
|
143
|
+
|
|
144
|
+
Example compose service:
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
services:
|
|
148
|
+
relay:
|
|
149
|
+
build: .
|
|
150
|
+
restart: unless-stopped
|
|
151
|
+
env_file: .env
|
|
152
|
+
expose:
|
|
153
|
+
- "6060"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`.env` should contain either a temporary shared key:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
RELAY_PORT=6060
|
|
160
|
+
RELAY_API_KEYS=<generated-shared-dev-key>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
or, preferably for production, explicit per-agent keys:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
RELAY_PORT=6060
|
|
167
|
+
RELAY_AGENTS="nexus-backend:<key>,builder-agent:<key>"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
If the app server already has Caddy/nginx, route a secure WebSocket subdomain to the relay container, for example:
|
|
171
|
+
|
|
172
|
+
```caddyfile
|
|
173
|
+
transport.perkos.xyz {
|
|
174
|
+
encode gzip zstd
|
|
175
|
+
|
|
176
|
+
@health path /health
|
|
177
|
+
respond @health "ok transport.perkos.xyz" 200
|
|
178
|
+
|
|
179
|
+
reverse_proxy perkos-a2a-relay:6060
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Caddy will automatically issue and renew Let's Encrypt certificates when DNS points to the server.
|
|
184
|
+
|
|
185
|
+
## Production hardening checklist
|
|
186
|
+
|
|
187
|
+
- Use explicit registered agents and per-agent registration keys.
|
|
188
|
+
- Keep callback endpoints authenticated and scoped by project/agent.
|
|
189
|
+
- Store task/message receipts with A2A task IDs for replay/debugging.
|
|
190
|
+
- Add relay health and peer discovery endpoints to the backend.
|
|
191
|
+
- Do not put secrets, private keys, seed phrases, or cloud credentials in A2A payloads.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# PerkOS A2A Pairing Registration
|
|
2
|
+
|
|
3
|
+
PerkOS A2A agent onboarding uses an invitation workflow instead of shared global relay keys.
|
|
4
|
+
|
|
5
|
+
## Core flow
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
Invite -> Agent Pairing -> Approval -> Registry Entry -> Scoped Relay Credential -> A2A Connection
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- **Relay/transport** moves messages and authenticates approved agent names.
|
|
12
|
+
- **Pairing registry** belongs to the external system, e.g. PerkOS Swarm, Nexus, or PerkyFi.
|
|
13
|
+
- **Agent identity** is generated locally by the agent. The private key never leaves the agent machine.
|
|
14
|
+
- **Approval** is explicit unless the invite was created with `autoApprove` for tests or trusted automation.
|
|
15
|
+
|
|
16
|
+
## System/admin: create an invite
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -X POST https://transport.perkos.xyz/pairing/invites \
|
|
20
|
+
-H 'authorization: Bearer <PAIRING_ADMIN_KEY>' \
|
|
21
|
+
-H 'content-type: application/json' \
|
|
22
|
+
-d '{
|
|
23
|
+
"system": "perkos-swarm",
|
|
24
|
+
"label": "Apollo joins Swarm Alpha",
|
|
25
|
+
"requestedScopes": ["a2a:connect", "swarm:join", "tasks:receive", "messages:send"],
|
|
26
|
+
"ttlMs": 900000
|
|
27
|
+
}'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Response:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"inviteId": "inv_...",
|
|
35
|
+
"system": "perkos-swarm",
|
|
36
|
+
"pairingUrl": "https://transport.perkos.xyz/pairing/invites/inv_...",
|
|
37
|
+
"relayUrl": "wss://transport.perkos.xyz/a2a",
|
|
38
|
+
"requestedScopes": ["a2a:connect", "swarm:join", "tasks:receive", "messages:send"]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Agent: claim the invite
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g @perkos/perkos-a2a
|
|
46
|
+
|
|
47
|
+
perkos-a2a-agent pair \
|
|
48
|
+
--invite https://transport.perkos.xyz/pairing/invites/inv_... \
|
|
49
|
+
--agent-name Apollo \
|
|
50
|
+
--runtime hermes \
|
|
51
|
+
--capabilities chat,code,research,tasks:receive,messages:send
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The CLI:
|
|
55
|
+
|
|
56
|
+
1. Creates or reuses a local Ed25519 identity at `~/.perkos/a2a/agents/<agent>.json`.
|
|
57
|
+
2. Sends `agentName`, `publicKey`, `runtime`, capabilities, and transport preference to the pairing URL.
|
|
58
|
+
3. Saves the approval credential when approved, or stores the pending request id.
|
|
59
|
+
|
|
60
|
+
## System/admin: review and approve
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
curl https://transport.perkos.xyz/pairing/requests \
|
|
64
|
+
-H 'authorization: Bearer <PAIRING_ADMIN_KEY>'
|
|
65
|
+
|
|
66
|
+
curl -X POST https://transport.perkos.xyz/pairing/requests/req_.../approve \
|
|
67
|
+
-H 'authorization: Bearer <PAIRING_ADMIN_KEY>' \
|
|
68
|
+
-H 'content-type: application/json' \
|
|
69
|
+
-d '{"scopes":["a2a:connect","swarm:join","tasks:receive","messages:send"]}'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Approval creates a registry entry and a relay credential scoped to that agent name.
|
|
73
|
+
|
|
74
|
+
## Agent: run after approval
|
|
75
|
+
|
|
76
|
+
The pairing CLI prints the bridge command:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
A2A_AGENT_NAME=Apollo \
|
|
80
|
+
A2A_RELAY_ENABLED=true \
|
|
81
|
+
A2A_RELAY_URL=wss://transport.perkos.xyz/a2a \
|
|
82
|
+
A2A_RELAY_API_KEY=<agent-scoped-relay-key> \
|
|
83
|
+
A2A_RUNTIME=hermes \
|
|
84
|
+
perkos-a2a-agent
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Prompt for an external agent
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
Install or enable @perkos/perkos-a2a.
|
|
91
|
+
Claim this invite: <PAIRING_URL>.
|
|
92
|
+
Generate your own local Ed25519 identity; never export your private key.
|
|
93
|
+
Register as agentName=<NAME>, runtime=<RUNTIME>, capabilities=<CAPABILITIES>.
|
|
94
|
+
Wait for approval from the inviting PerkOS system.
|
|
95
|
+
After approval, connect to the relay using the scoped credential returned by pairing.
|
|
96
|
+
Report final state: agentId, system, relay URL, scopes, runtime, and connection status.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Environment for the pairing transport service
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
RELAY_PORT=6060
|
|
103
|
+
PUBLIC_BASE_URL=https://transport.perkos.xyz
|
|
104
|
+
PUBLIC_RELAY_URL=wss://transport.perkos.xyz/a2a
|
|
105
|
+
PAIRING_ENABLED=true
|
|
106
|
+
PAIRING_DB_PATH=/var/lib/perkos-transport/pairing-registry.json
|
|
107
|
+
PAIRING_ADMIN_KEYS=<admin-key-1>,<admin-key-2>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`PAIRING_ADMIN_KEYS` should be required in production. If omitted, invite/admin endpoints are open and should only be used for local development.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@perkos/perkos-a2a",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.15",
|
|
4
4
|
"description": "A2A Protocol communication plugin for OpenClaw — Agent-to-Agent protocol implementation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
10
10
|
"bin",
|
|
11
|
+
"docs",
|
|
11
12
|
"skills",
|
|
12
13
|
"openclaw.plugin.json"
|
|
13
14
|
],
|