@perkos/perkos-a2a 0.8.11 → 0.8.13

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 CHANGED
@@ -117,6 +117,8 @@ openclaw perkos-a2a status
117
117
  "config": {
118
118
  "agentName": "my-agent",
119
119
  "port": 5050,
120
+ "bindHost": "0.0.0.0",
121
+ "publicUrl": "https://my-agent.example.com",
120
122
  "mode": "auto",
121
123
  "skills": [
122
124
  {
@@ -140,6 +142,10 @@ openclaw perkos-a2a status
140
142
  "url": "wss://relay.example.com:8787",
141
143
  "apiKey": "relay-api-key",
142
144
  "enabled": true
145
+ },
146
+ "runtime": {
147
+ "kind": "openclaw",
148
+ "sessionKey": "agent:main"
143
149
  }
144
150
  }
145
151
  }
@@ -151,7 +157,9 @@ openclaw perkos-a2a status
151
157
  | Option | Type | Default | Description |
152
158
  |---|---|---|---|
153
159
  | `agentName` | string | `"agent"` | This agent's name in the network |
154
- | `port` | number | `5050` | HTTP server port (avoid 5000 on macOS AirPlay) |
160
+ | `port` | number | `5050` | HTTP server port. Use one unique port per agent/container on shared VPS hosts |
161
+ | `bindHost` | string | `0.0.0.0` | Interface to bind. Use `0.0.0.0` in Docker, `127.0.0.1` behind local reverse proxies/tunnels |
162
+ | `publicUrl` | string | — | Externally reachable base URL advertised in Agent Card; use DNS/tunnel/LAN URL when localhost is not reachable |
155
163
  | `mode` | string | `"auto"` | Operating mode: `auto`, `full`, `client-only`, `relay` |
156
164
  | `skills` | array | `[]` | Skills exposed via the agent card |
157
165
  | `peers` | object | `{}` | Map of peer names → A2A base URLs |
@@ -161,6 +169,91 @@ openclaw perkos-a2a status
161
169
  | `relay.url` | string | — | Relay hub WebSocket URL |
162
170
  | `relay.apiKey` | string | — | API key for relay hub authentication |
163
171
  | `relay.enabled` | boolean | `false` | Enable relay connectivity |
172
+ | `runtime.kind` | string | `"openclaw"` | Inbound execution target: `openclaw`, `hermes`, or `none` |
173
+ | `runtime.sessionKey` | string | runtime-specific | Target session (`agent:main` for OpenClaw, `main` for Hermes) |
174
+ | `runtime.hermesUrl` | string | `http://127.0.0.1:3000` | Hermes Workspace URL when `runtime.kind = "hermes"` |
175
+ | `runtime.hermesToken` | string | — | Optional Hermes Workspace session token, sent as `claude-auth` cookie |
176
+ | `runtime.hermesEndpoint` | string | `/api/session-send` | Hermes Workspace send endpoint |
177
+
178
+ ## OpenClaw ↔ Hermes Delivery
179
+
180
+ PerkOS A2A now separates the transport from the local runtime. Direct HTTP and relay routing stay the same, but inbound tasks can be delivered into either OpenClaw or Hermes Workspace.
181
+
182
+ ### OpenClaw receiver
183
+
184
+ ```json
185
+ "runtime": {
186
+ "kind": "openclaw",
187
+ "sessionKey": "agent:main"
188
+ }
189
+ ```
190
+
191
+ OpenClaw uses `enqueueSystemEvent` + `requestHeartbeatNow` when available.
192
+
193
+ ### Hermes receiver
194
+
195
+ ```json
196
+ "runtime": {
197
+ "kind": "hermes",
198
+ "sessionKey": "main",
199
+ "hermesUrl": "http://127.0.0.1:3000",
200
+ "hermesToken": "OPTIONAL_WORKSPACE_SESSION_TOKEN"
201
+ }
202
+ ```
203
+
204
+ Hermes delivery posts to `/api/session-send`, which forwards into Hermes Workspace's streaming chat path. If the Workspace has password protection enabled, provide the persisted workspace session token as `runtime.hermesToken` or `HERMES_WORKSPACE_TOKEN`.
205
+
206
+ Hermes does not load OpenClaw plugins, so run the standalone bridge next to Hermes Workspace:
207
+
208
+ ```bash
209
+ A2A_AGENT_NAME=hermes-agent \
210
+ A2A_RUNTIME=hermes \
211
+ A2A_MODE=client-only \
212
+ A2A_RELAY_ENABLED=true \
213
+ A2A_RELAY_URL=wss://relay.example.com \
214
+ A2A_RELAY_API_KEY=*** \
215
+ HERMES_WORKSPACE_URL=http://127.0.0.1:3000 \
216
+ perkos-a2a-agent
217
+ ```
218
+
219
+ The bridge keeps an outbound relay WebSocket open, so agents behind NAT or dynamic IPs receive tasks without cron polling or inbound port forwarding.
220
+
221
+ ## Deployment Reality: Docker, VPS, NAT, and Dynamic IPs
222
+
223
+ PerkOS A2A must not assume one public machine equals one agent. Real deployments often run many agents as Docker containers on one VPS, or several local machines behind one office/home NAT.
224
+
225
+ ### Multiple Docker agents on one VPS
226
+
227
+ Each agent needs a unique internal/listening port and, if exposed through the host, a unique host port or reverse-proxy route.
228
+
229
+ ```yaml
230
+ services:
231
+ morpheus:
232
+ image: openclaw-agent
233
+ ports:
234
+ - "127.0.0.1:5050:5050"
235
+ environment:
236
+ A2A_AGENT_NAME: morpheus
237
+
238
+ neo:
239
+ image: openclaw-agent
240
+ ports:
241
+ - "127.0.0.1:5051:5050"
242
+ environment:
243
+ A2A_AGENT_NAME: neo
244
+ ```
245
+
246
+ Recommended pattern: keep container ports private, put Caddy/Nginx/Traefik in front, and set each agent's `publicUrl` to its stable route, e.g. `https://morpheus-a2a.example.com`.
247
+
248
+ ### NAT / changing public IP
249
+
250
+ For office/home/local agents behind NAT, direct P2P is brittle because all machines share one external IP and that IP can change. Use one of these patterns:
251
+
252
+ 1. **Relay hub** — preferred default. Every agent opens an outbound WebSocket to the relay; no inbound ports or static IP required.
253
+ 2. **Tunnel/DNS** — Cloudflare Tunnel, Tailscale Funnel, ngrok, or similar. Set `publicUrl` to the stable tunnel hostname.
254
+ 3. **Direct LAN/VPN only** — okay for trusted LAN/Tailscale networks, but still require API keys.
255
+
256
+ Do not expose unauthenticated A2A ports publicly. If direct HTTP is used, pair `auth.requireApiKey` with per-peer `peerAuth`.
164
257
 
165
258
  ## Modes
166
259
 
@@ -254,6 +347,39 @@ Agent A Agent B
254
347
  └─────────────┘ └─────────────┘
255
348
  ```
256
349
 
350
+ ## Registrar / Rendezvous Security Model
351
+
352
+ The relay hub should be treated as a **registrar/rendezvous server**, not as an unrestricted public chat server. Its jobs are:
353
+
354
+ 1. Keep presence: which approved agents are currently connected.
355
+ 2. Route frames when direct HTTP is impossible because of NAT, Docker, or dynamic IPs.
356
+ 3. Reject unapproved agents before they can discover or message anyone.
357
+
358
+ In production, prefer an explicit approved-agent registry instead of one shared relay key:
359
+
360
+ ```bash
361
+ a2a-relay \
362
+ --port 6060 \
363
+ --agents morpheus:KEY_FOR_MORPHEUS,neo:KEY_FOR_NEO,hermes-agent:KEY_FOR_HERMES
364
+ ```
365
+
366
+ Or via environment:
367
+
368
+ ```bash
369
+ RELAY_AGENTS="morpheus:KEY_FOR_MORPHEUS,neo:KEY_FOR_NEO,hermes-agent:KEY_FOR_HERMES" a2a-relay
370
+ ```
371
+
372
+ With `registeredAgents` enabled:
373
+
374
+ - An agent can only register under its approved name with its own key.
375
+ - A stolen/shared key cannot impersonate another agent name.
376
+ - Messages to unapproved target names are rejected.
377
+ - Discovery only returns currently connected approved agents.
378
+
379
+ A future pairing flow can generate these entries automatically: human approves an agent card/fingerprint, server stores `agentName -> registration key`, and only then can that agent connect.
380
+
381
+ For Nexus-style product backends, see [`docs/nexus-communications-server.md`](docs/nexus-communications-server.md) for the communications-server pattern: backend orchestrator → A2A relay → OpenClaw/Hermes runtime worker → authenticated backend callbacks.
382
+
257
383
  ### Relay Hub (NAT Traversal)
258
384
 
259
385
  Agents behind NAT connect outbound to the relay hub via WebSocket. No port forwarding needed.
package/bin/agent.ts ADDED
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone PerkOS A2A bridge.
4
+ *
5
+ * This is for runtimes that do not load OpenClaw plugins directly, especially
6
+ * Hermes Workspace. It runs the same A2A HTTP/relay server and delivers inbound
7
+ * tasks into a local runtime over that runtime's native wake/send API.
8
+ *
9
+ * Usage:
10
+ * perkos-a2a-agent --config ./a2a.config.json
11
+ * A2A_AGENT_NAME=hermes A2A_RUNTIME=hermes HERMES_WORKSPACE_URL=http://127.0.0.1:3000 perkos-a2a-agent
12
+ */
13
+
14
+ import { readFileSync } from "fs";
15
+ import { randomUUID } from "crypto";
16
+ import { A2AServer } from "../src/server.js";
17
+ import type { A2APluginConfig, Task } from "../src/types.js";
18
+
19
+ function argValue(flag: string): string | undefined {
20
+ const idx = process.argv.indexOf(flag);
21
+ if (idx === -1) return undefined;
22
+ return process.argv[idx + 1];
23
+ }
24
+
25
+ function parseJsonEnv<T>(name: string, fallback: T): T {
26
+ const raw = process.env[name];
27
+ if (!raw) return fallback;
28
+ try {
29
+ return JSON.parse(raw) as T;
30
+ } catch (err) {
31
+ throw new Error(`${name} must be valid JSON: ${err instanceof Error ? err.message : String(err)}`);
32
+ }
33
+ }
34
+
35
+ function loadConfig(): A2APluginConfig {
36
+ const configPath = argValue("--config") || argValue("-c") || process.env.A2A_CONFIG;
37
+ if (configPath) {
38
+ return JSON.parse(readFileSync(configPath, "utf8")) as A2APluginConfig;
39
+ }
40
+
41
+ const port = Number(process.env.A2A_PORT || "5050");
42
+ const relayEnabled = String(process.env.A2A_RELAY_ENABLED || "").toLowerCase() === "true";
43
+ return {
44
+ agentName: process.env.A2A_AGENT_NAME || "agent",
45
+ port,
46
+ bindHost: process.env.A2A_BIND_HOST || "0.0.0.0",
47
+ publicUrl: process.env.A2A_PUBLIC_URL,
48
+ mode: (process.env.A2A_MODE as A2APluginConfig["mode"]) || "auto",
49
+ skills: parseJsonEnv("A2A_SKILLS", []),
50
+ peers: parseJsonEnv("A2A_PEERS", {}),
51
+ peerAuth: parseJsonEnv("A2A_PEER_AUTH", {}),
52
+ auth: process.env.A2A_API_KEYS
53
+ ? { requireApiKey: true, apiKeys: process.env.A2A_API_KEYS.split(",").map((v) => v.trim()).filter(Boolean) }
54
+ : undefined,
55
+ relay: relayEnabled || process.env.A2A_RELAY_URL
56
+ ? {
57
+ enabled: relayEnabled || Boolean(process.env.A2A_RELAY_URL),
58
+ url: process.env.A2A_RELAY_URL || "",
59
+ apiKey: process.env.A2A_RELAY_API_KEY || "",
60
+ hubPort: process.env.A2A_RELAY_HUB_PORT ? Number(process.env.A2A_RELAY_HUB_PORT) : undefined,
61
+ hubApiKeys: process.env.A2A_RELAY_HUB_API_KEYS?.split(",").map((v) => v.trim()).filter(Boolean),
62
+ }
63
+ : undefined,
64
+ runtime: {
65
+ kind: (process.env.A2A_RUNTIME as A2APluginConfig["runtime"] extends infer R ? R extends { kind?: infer K } ? K : never : never) || "hermes",
66
+ sessionKey: process.env.A2A_SESSION_KEY,
67
+ hermesUrl: process.env.HERMES_WORKSPACE_URL,
68
+ hermesToken: process.env.HERMES_WORKSPACE_TOKEN || process.env.HERMES_SESSION_TOKEN,
69
+ hermesEndpoint: process.env.HERMES_WORKSPACE_ENDPOINT,
70
+ },
71
+ };
72
+ }
73
+
74
+ function taskMarker(task: Task): string {
75
+ return `[A2A_RESULT:${task.id}]`;
76
+ }
77
+
78
+ function buildRuntimeMessage(task: Task, text: string): string {
79
+ const from = (task.metadata?.fromAgent as string) || "unknown";
80
+ const marker = taskMarker(task);
81
+ return [
82
+ marker,
83
+ `From: ${from}`,
84
+ `Task ID: ${task.id}`,
85
+ `Context ID: ${task.contextId}`,
86
+ "",
87
+ "Execute the following request and include the final answer in your assistant reply.",
88
+ `Your final answer must begin with exactly: ${marker}`,
89
+ "Return the useful final answer after that marker.",
90
+ "",
91
+ text,
92
+ ].join("\n");
93
+ }
94
+
95
+ async function deliverToHermes(config: A2APluginConfig, message: string): Promise<void> {
96
+ const runtime = config.runtime || {};
97
+ const baseUrl = (runtime.hermesUrl || process.env.HERMES_WORKSPACE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
98
+ const endpoint = runtime.hermesEndpoint || "/api/session-send";
99
+ const sessionKey = runtime.sessionKey || "main";
100
+ const token = runtime.hermesToken || process.env.HERMES_WORKSPACE_TOKEN || process.env.HERMES_SESSION_TOKEN;
101
+ const url = `${baseUrl}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
102
+ const headers: Record<string, string> = { "content-type": "application/json" };
103
+ if (token) headers.cookie = `claude-auth=${token}`;
104
+
105
+ const response = await fetch(url, {
106
+ method: "POST",
107
+ headers,
108
+ body: JSON.stringify({ sessionKey, message }),
109
+ });
110
+ if (!response.ok) {
111
+ const body = await response.text().catch(() => "");
112
+ throw new Error(`Hermes delivery failed (${response.status}): ${body || response.statusText}`);
113
+ }
114
+ }
115
+
116
+ async function main(): Promise<void> {
117
+ const config = loadConfig();
118
+ const logger = console;
119
+ const server = new A2AServer(config, logger);
120
+
121
+ server.setTaskResultHandler(async (task, text) => {
122
+ const runtimeKind = config.runtime?.kind || "hermes";
123
+ task.status = {
124
+ state: "working",
125
+ timestamp: new Date().toISOString(),
126
+ message: { role: "agent", parts: [{ kind: "text", text: `Task dispatched to ${runtimeKind} runtime.` }] },
127
+ };
128
+
129
+ if (runtimeKind === "hermes") {
130
+ await deliverToHermes(config, buildRuntimeMessage(task, text));
131
+ task.artifacts.push({
132
+ kind: "artifact",
133
+ artifactId: randomUUID(),
134
+ parts: [{ kind: "text", text: "Delivered to Hermes Workspace." }],
135
+ });
136
+ return;
137
+ }
138
+
139
+ if (runtimeKind === "none") {
140
+ task.artifacts.push({
141
+ kind: "artifact",
142
+ artifactId: randomUUID(),
143
+ parts: [{ kind: "text", text: "Runtime delivery disabled." }],
144
+ });
145
+ return;
146
+ }
147
+
148
+ throw new Error(`Standalone runtime '${runtimeKind}' is not supported yet. Use OpenClaw plugin mode for OpenClaw delivery.`);
149
+ });
150
+
151
+ server.setTaskFailureHandler((task, errorText) => {
152
+ task.status = {
153
+ state: "failed",
154
+ timestamp: new Date().toISOString(),
155
+ message: { role: "agent", parts: [{ kind: "text", text: errorText }] },
156
+ };
157
+ });
158
+
159
+ await server.start();
160
+ logger.info(`[perkos-a2a] standalone bridge running for ${config.agentName} (${config.runtime?.kind || "hermes"})`);
161
+
162
+ const stop = () => {
163
+ logger.info("[perkos-a2a] standalone bridge shutting down");
164
+ process.exit(0);
165
+ };
166
+ process.on("SIGINT", stop);
167
+ process.on("SIGTERM", stop);
168
+ }
169
+
170
+ main().catch((err) => {
171
+ console.error(`[perkos-a2a] standalone bridge failed: ${err instanceof Error ? err.stack || err.message : String(err)}`);
172
+ process.exit(1);
173
+ });
package/bin/relay.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * Environment variables:
9
9
  * RELAY_PORT - WebSocket port (default: 6060)
10
10
  * RELAY_API_KEYS - Comma-separated list of accepted API keys
11
+ * RELAY_AGENTS - Comma-separated approved agents as name:key,name2:key2
11
12
  * RELAY_MAX_QUEUE - Max queued messages per offline agent (default: 200)
12
13
  * RELAY_RATE_LIMIT - Max messages per agent per minute (default: 60)
13
14
  */
@@ -28,6 +29,14 @@ function parseArgs(): Partial<RelayHubConfig> {
28
29
  case "--api-keys":
29
30
  config.apiKeys = args[++i].split(",").map((k) => k.trim()).filter(Boolean);
30
31
  break;
32
+ case "--agents":
33
+ config.registeredAgents = Object.fromEntries(
34
+ args[++i].split(",").map((entry) => {
35
+ const [name, ...keyParts] = entry.split(":");
36
+ return [name.trim(), keyParts.join(":").trim()];
37
+ }).filter(([name, key]) => name && key)
38
+ );
39
+ break;
31
40
  case "--max-queue":
32
41
  config.maxQueuePerAgent = parseInt(args[++i], 10);
33
42
  break;
@@ -40,7 +49,8 @@ function parseArgs(): Partial<RelayHubConfig> {
40
49
  console.log("");
41
50
  console.log("Options:");
42
51
  console.log(" --port, -p <port> WebSocket port (default: 6060)");
43
- console.log(" --api-keys <k1,k2,...> Accepted API keys (default: none, auth disabled)");
52
+ console.log(" --api-keys <k1,k2,...> Accepted API keys (legacy shared-key mode)");
53
+ console.log(" --agents <name:key,...> Approved agent registry (recommended)");
44
54
  console.log(" --max-queue <n> Max queued messages per offline agent (default: 200)");
45
55
  console.log(" --rate-limit <n> Max messages per agent per minute (default: 60)");
46
56
  console.log("");
@@ -57,6 +67,14 @@ function parseArgs(): Partial<RelayHubConfig> {
57
67
  if (!config.apiKeys && process.env.RELAY_API_KEYS) {
58
68
  config.apiKeys = process.env.RELAY_API_KEYS.split(",").map((k) => k.trim()).filter(Boolean);
59
69
  }
70
+ if (!config.registeredAgents && process.env.RELAY_AGENTS) {
71
+ config.registeredAgents = Object.fromEntries(
72
+ process.env.RELAY_AGENTS.split(",").map((entry) => {
73
+ const [name, ...keyParts] = entry.split(":");
74
+ return [name.trim(), keyParts.join(":").trim()];
75
+ }).filter(([name, key]) => name && key)
76
+ );
77
+ }
60
78
  if (!config.maxQueuePerAgent && process.env.RELAY_MAX_QUEUE) {
61
79
  config.maxQueuePerAgent = parseInt(process.env.RELAY_MAX_QUEUE, 10);
62
80
  }
@@ -74,7 +92,7 @@ hub.start();
74
92
 
75
93
  console.log("[perkos-a2a] Relay hub running");
76
94
  console.log(`[perkos-a2a] Port: ${config.port || 6060}`);
77
- console.log(`[perkos-a2a] Auth: ${config.apiKeys?.length ? "enabled" : "disabled (no API keys configured)"}`);
95
+ console.log(`[perkos-a2a] Auth: ${config.registeredAgents && Object.keys(config.registeredAgents).length ? "approved-agent registry" : config.apiKeys?.length ? "shared-key" : "disabled (no API keys configured)"}`);
78
96
 
79
97
  process.on("SIGINT", () => {
80
98
  console.log("\n[perkos-a2a] Shutting down relay hub...");
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone PerkOS A2A bridge.
4
+ *
5
+ * This is for runtimes that do not load OpenClaw plugins directly, especially
6
+ * Hermes Workspace. It runs the same A2A HTTP/relay server and delivers inbound
7
+ * tasks into a local runtime over that runtime's native wake/send API.
8
+ *
9
+ * Usage:
10
+ * perkos-a2a-agent --config ./a2a.config.json
11
+ * A2A_AGENT_NAME=hermes A2A_RUNTIME=hermes HERMES_WORKSPACE_URL=http://127.0.0.1:3000 perkos-a2a-agent
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG"}
package/dist/agent.js ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone PerkOS A2A bridge.
4
+ *
5
+ * This is for runtimes that do not load OpenClaw plugins directly, especially
6
+ * Hermes Workspace. It runs the same A2A HTTP/relay server and delivers inbound
7
+ * tasks into a local runtime over that runtime's native wake/send API.
8
+ *
9
+ * Usage:
10
+ * perkos-a2a-agent --config ./a2a.config.json
11
+ * A2A_AGENT_NAME=hermes A2A_RUNTIME=hermes HERMES_WORKSPACE_URL=http://127.0.0.1:3000 perkos-a2a-agent
12
+ */
13
+ import { readFileSync } from "fs";
14
+ import { randomUUID } from "crypto";
15
+ import { A2AServer } from "./server.js";
16
+ import { completeTaskWithReply, generateRuntimeReply } from "./runtime-reply.js";
17
+ function argValue(flag) {
18
+ const idx = process.argv.indexOf(flag);
19
+ if (idx === -1)
20
+ return undefined;
21
+ return process.argv[idx + 1];
22
+ }
23
+ function parseJsonEnv(name, fallback) {
24
+ const raw = process.env[name];
25
+ if (!raw)
26
+ return fallback;
27
+ try {
28
+ return JSON.parse(raw);
29
+ }
30
+ catch (err) {
31
+ throw new Error(`${name} must be valid JSON: ${err instanceof Error ? err.message : String(err)}`);
32
+ }
33
+ }
34
+ function loadConfig() {
35
+ const configPath = argValue("--config") || argValue("-c") || process.env.A2A_CONFIG;
36
+ if (configPath) {
37
+ return JSON.parse(readFileSync(configPath, "utf8"));
38
+ }
39
+ const port = Number(process.env.A2A_PORT || "5050");
40
+ const relayEnabled = String(process.env.A2A_RELAY_ENABLED || "").toLowerCase() === "true";
41
+ return {
42
+ agentName: process.env.A2A_AGENT_NAME || "agent",
43
+ port,
44
+ bindHost: process.env.A2A_BIND_HOST || "0.0.0.0",
45
+ publicUrl: process.env.A2A_PUBLIC_URL,
46
+ mode: process.env.A2A_MODE || "auto",
47
+ skills: parseJsonEnv("A2A_SKILLS", []),
48
+ peers: parseJsonEnv("A2A_PEERS", {}),
49
+ peerAuth: parseJsonEnv("A2A_PEER_AUTH", {}),
50
+ auth: process.env.A2A_API_KEYS
51
+ ? { requireApiKey: true, apiKeys: process.env.A2A_API_KEYS.split(",").map((v) => v.trim()).filter(Boolean) }
52
+ : undefined,
53
+ relay: relayEnabled || process.env.A2A_RELAY_URL
54
+ ? {
55
+ enabled: relayEnabled || Boolean(process.env.A2A_RELAY_URL),
56
+ url: process.env.A2A_RELAY_URL || "",
57
+ apiKey: process.env.A2A_RELAY_API_KEY || "",
58
+ hubPort: process.env.A2A_RELAY_HUB_PORT ? Number(process.env.A2A_RELAY_HUB_PORT) : undefined,
59
+ hubApiKeys: process.env.A2A_RELAY_HUB_API_KEYS?.split(",").map((v) => v.trim()).filter(Boolean),
60
+ }
61
+ : undefined,
62
+ runtime: {
63
+ kind: process.env.A2A_RUNTIME || "hermes",
64
+ sessionKey: process.env.A2A_SESSION_KEY,
65
+ hermesUrl: process.env.HERMES_WORKSPACE_URL,
66
+ hermesToken: process.env.HERMES_WORKSPACE_TOKEN || process.env.HERMES_SESSION_TOKEN,
67
+ hermesEndpoint: process.env.HERMES_WORKSPACE_ENDPOINT,
68
+ },
69
+ };
70
+ }
71
+ function taskMarker(task) {
72
+ return `[A2A_RESULT:${task.id}]`;
73
+ }
74
+ function buildRuntimeMessage(task, text) {
75
+ const from = task.metadata?.fromAgent || "unknown";
76
+ const marker = taskMarker(task);
77
+ return [
78
+ marker,
79
+ `From: ${from}`,
80
+ `Task ID: ${task.id}`,
81
+ `Context ID: ${task.contextId}`,
82
+ "",
83
+ "Execute the following request and include the final answer in your assistant reply.",
84
+ `Your final answer must begin with exactly: ${marker}`,
85
+ "Return the useful final answer after that marker.",
86
+ "",
87
+ text,
88
+ ].join("\n");
89
+ }
90
+ async function getHermesAuthCookie(baseUrl, explicitToken) {
91
+ if (explicitToken)
92
+ return `claude-auth=${explicitToken}`;
93
+ const password = process.env.HERMES_PASSWORD || process.env.CLAUDE_PASSWORD;
94
+ if (!password)
95
+ return undefined;
96
+ const response = await fetch(`${baseUrl}/api/auth`, {
97
+ method: "POST",
98
+ headers: { "content-type": "application/json" },
99
+ body: JSON.stringify({ password }),
100
+ });
101
+ if (!response.ok) {
102
+ const body = await response.text().catch(() => "");
103
+ throw new Error(`Hermes auth failed (${response.status}): ${body || response.statusText}`);
104
+ }
105
+ const setCookie = response.headers.get("set-cookie");
106
+ return setCookie?.split(";")[0];
107
+ }
108
+ async function deliverToHermes(config, message) {
109
+ const runtime = config.runtime || {};
110
+ const baseUrl = (runtime.hermesUrl || process.env.HERMES_WORKSPACE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
111
+ const endpoint = runtime.hermesEndpoint || "/api/session-send";
112
+ const sessionKey = runtime.sessionKey || "main";
113
+ const token = runtime.hermesToken || process.env.HERMES_WORKSPACE_TOKEN || process.env.HERMES_SESSION_TOKEN;
114
+ const url = `${baseUrl}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
115
+ const headers = { "content-type": "application/json" };
116
+ const cookie = await getHermesAuthCookie(baseUrl, token);
117
+ if (cookie)
118
+ headers.cookie = cookie;
119
+ const response = await fetch(url, {
120
+ method: "POST",
121
+ headers,
122
+ body: JSON.stringify({ sessionKey, message }),
123
+ });
124
+ if (!response.ok) {
125
+ const body = await response.text().catch(() => "");
126
+ throw new Error(`Hermes delivery failed (${response.status}): ${body || response.statusText}`);
127
+ }
128
+ }
129
+ async function main() {
130
+ const config = loadConfig();
131
+ const logger = console;
132
+ const server = new A2AServer(config, logger);
133
+ server.setTaskResultHandler(async (task, text) => {
134
+ const runtimeKind = config.runtime?.kind || "hermes";
135
+ task.status = {
136
+ state: "working",
137
+ timestamp: new Date().toISOString(),
138
+ message: { role: "agent", parts: [{ kind: "text", text: `Task dispatched to ${runtimeKind} runtime.` }] },
139
+ };
140
+ if (runtimeKind === "hermes") {
141
+ await deliverToHermes(config, buildRuntimeMessage(task, text));
142
+ task.artifacts.push({
143
+ kind: "artifact",
144
+ artifactId: randomUUID(),
145
+ parts: [{ kind: "text", text: "Delivered to Hermes Workspace." }],
146
+ });
147
+ const finalText = await generateRuntimeReply(config, text, taskMarker(task));
148
+ completeTaskWithReply(task, finalText);
149
+ return;
150
+ }
151
+ if (runtimeKind === "none") {
152
+ task.artifacts.push({
153
+ kind: "artifact",
154
+ artifactId: randomUUID(),
155
+ parts: [{ kind: "text", text: "Runtime delivery disabled." }],
156
+ });
157
+ return;
158
+ }
159
+ throw new Error(`Standalone runtime '${runtimeKind}' is not supported yet. Use OpenClaw plugin mode for OpenClaw delivery.`);
160
+ });
161
+ server.setTaskFailureHandler((task, errorText) => {
162
+ task.status = {
163
+ state: "failed",
164
+ timestamp: new Date().toISOString(),
165
+ message: { role: "agent", parts: [{ kind: "text", text: errorText }] },
166
+ };
167
+ });
168
+ await server.start();
169
+ logger.info(`[perkos-a2a] standalone bridge running for ${config.agentName} (${config.runtime?.kind || "hermes"})`);
170
+ const stop = () => {
171
+ logger.info("[perkos-a2a] standalone bridge shutting down");
172
+ process.exit(0);
173
+ };
174
+ process.on("SIGINT", stop);
175
+ process.on("SIGTERM", stop);
176
+ }
177
+ main().catch((err) => {
178
+ console.error(`[perkos-a2a] standalone bridge failed: ${err instanceof Error ? err.stack || err.message : String(err)}`);
179
+ process.exit(1);
180
+ });
181
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,cAAc,YAAY,CAAC;AAuB3B,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAG,EAAE,GAAG,QA0exC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,cAAc,YAAY,CAAC;AA6D3B,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAG,EAAE,GAAG,QAwgBxC"}