a2acalling 0.3.4 → 0.3.6

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
@@ -58,10 +58,10 @@ npm install -g a2acalling
58
58
  npm install a2acalling
59
59
  ```
60
60
 
61
- ### For OpenClaw Users
61
+ ### Setup (Auto-Detect Runtime)
62
62
 
63
63
  ```bash
64
- # Auto setup (detects gateway and configures dashboard location)
64
+ # Auto setup (detects OpenClaw gateway/runtime or configures standalone mode)
65
65
  npx a2acalling setup
66
66
 
67
67
  # Or clone and install
@@ -72,8 +72,9 @@ node scripts/install-openclaw.js setup
72
72
  ```
73
73
 
74
74
  Setup behavior:
75
+ - Runtime auto-detects OpenClaw when available and falls back to generic mode if unavailable.
75
76
  - If OpenClaw gateway is detected, dashboard is exposed on gateway at `/a2a` (proxied to A2A backend).
76
- - If gateway is not detected, dashboard is served directly by A2A server at `/dashboard`.
77
+ - If OpenClaw is not detected, setup bootstraps standalone config + bridge templates and serves dashboard at `/dashboard`.
77
78
  - Setup prints the exact dashboard URL at the end.
78
79
 
79
80
  Before the first `a2a call`, the owner must set permissions and disclosure tiers. Run onboarding first:
@@ -325,6 +326,14 @@ app.listen(3001);
325
326
  | `A2A_HOSTNAME` | Hostname for invite URLs (required for creates) |
326
327
  | `A2A_PORT` | Server port (default: 3001) |
327
328
  | `A2A_CONFIG_DIR` | Config directory (default: `~/.config/openclaw`) |
329
+ | `A2A_WORKSPACE` | Workspace root for context files like `USER.md` (default: current directory) |
330
+ | `A2A_RUNTIME` | Runtime mode: `auto` (default), `openclaw`, or `generic` |
331
+ | `A2A_RUNTIME_FAILOVER` | Fallback to generic runtime if OpenClaw runtime errors (default: `true`) |
332
+ | `A2A_AGENT_COMMAND` | Generic runtime command for inbound turn handling (reads JSON from stdin) |
333
+ | `A2A_SUMMARY_COMMAND` | Generic runtime command for call summaries (reads JSON from stdin) |
334
+ | `A2A_NOTIFY_COMMAND` | Generic runtime command for owner notifications (reads JSON from stdin) |
335
+ | `A2A_AGENT_NAME` | Override local agent display name |
336
+ | `A2A_OWNER_NAME` | Override owner display name |
328
337
  | `A2A_COLLAB_MODE` | Conversation style: `adaptive` (default) or `deep_dive` |
329
338
  | `A2A_ADMIN_TOKEN` | Protect dashboard/conversation admin routes for non-local access |
330
339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * A2A Calling - OpenClaw Integration Installer
3
+ * A2A Calling Setup Installer
4
4
  *
5
5
  * Supports automatic setup:
6
6
  * - If OpenClaw gateway is detected, install a gateway HTTP proxy plugin
7
7
  * so dashboard is accessible at /a2a on gateway.
8
8
  * - If gateway is not detected, dashboard runs on standalone A2A server.
9
+ * - If OpenClaw is not installed, bootstrap standalone runtime templates.
9
10
  *
10
11
  * Usage:
11
12
  * npx a2acalling install
@@ -16,6 +17,7 @@
16
17
 
17
18
  const fs = require('fs');
18
19
  const path = require('path');
20
+ const crypto = require('crypto');
19
21
  const { execSync } = require('child_process');
20
22
 
21
23
  // Paths
@@ -111,6 +113,112 @@ function resolveGatewayBaseUrl() {
111
113
  return `http://127.0.0.1:${fallbackPort}`;
112
114
  }
113
115
 
116
+ function resolveA2AConfigDir() {
117
+ return process.env.A2A_CONFIG_DIR ||
118
+ process.env.OPENCLAW_CONFIG_DIR ||
119
+ path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
120
+ }
121
+
122
+ function safeRead(filePath) {
123
+ try {
124
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
125
+ } catch (err) {
126
+ return '';
127
+ }
128
+ }
129
+
130
+ function writeExecutableFile(filePath, content) {
131
+ fs.writeFileSync(filePath, content, { mode: 0o755 });
132
+ try {
133
+ fs.chmodSync(filePath, 0o755);
134
+ } catch (err) {
135
+ // Non-fatal on platforms without chmod support.
136
+ }
137
+ }
138
+
139
+ function ensureStandaloneBootstrap(hostname, port) {
140
+ const configDir = resolveA2AConfigDir();
141
+ ensureDir(configDir);
142
+
143
+ const configFile = path.join(configDir, 'a2a-config.json');
144
+ const manifestFile = path.join(configDir, 'a2a-disclosure.json');
145
+
146
+ try {
147
+ const { A2AConfig } = require('../src/lib/config');
148
+ const { loadManifest, saveManifest, generateDefaultManifest } = require('../src/lib/disclosure');
149
+
150
+ const config = new A2AConfig();
151
+ const defaults = config.getDefaults() || {};
152
+ config.setDefaults(defaults);
153
+
154
+ const agent = config.getAgent() || {};
155
+ if (!agent.hostname) {
156
+ config.setAgent({
157
+ hostname: `${hostname}:${port}`
158
+ });
159
+ }
160
+
161
+ const manifest = loadManifest();
162
+ if (!manifest || Object.keys(manifest).length === 0) {
163
+ const generated = generateDefaultManifest({
164
+ user: safeRead(path.join(process.cwd(), 'USER.md')),
165
+ heartbeat: safeRead(path.join(process.cwd(), 'HEARTBEAT.md')),
166
+ soul: safeRead(path.join(process.cwd(), 'SOUL.md'))
167
+ });
168
+ saveManifest(generated);
169
+ log(`Generated default disclosure manifest: ${manifestFile}`);
170
+ }
171
+ } catch (err) {
172
+ warn(`Standalone config bootstrap failed: ${err.message}`);
173
+ }
174
+
175
+ const bridgeDir = path.join(configDir, 'runtime-bridge');
176
+ ensureDir(bridgeDir);
177
+ const turnScript = path.join(bridgeDir, 'a2a-turn.sh');
178
+ const summaryScript = path.join(bridgeDir, 'a2a-summary.sh');
179
+ const notifyScript = path.join(bridgeDir, 'a2a-notify.sh');
180
+
181
+ if (!fs.existsSync(turnScript)) {
182
+ writeExecutableFile(turnScript, `#!/usr/bin/env bash
183
+ set -euo pipefail
184
+ payload="$(cat || true)"
185
+ echo '{"response":"Generic bridge placeholder: your agent bridge is active. I received your message and can continue the call. What collaboration outcome should we target?"}'
186
+ `);
187
+ }
188
+
189
+ if (!fs.existsSync(summaryScript)) {
190
+ writeExecutableFile(summaryScript, `#!/usr/bin/env bash
191
+ set -euo pipefail
192
+ payload="$(cat || true)"
193
+ echo '{"summary":"Generic summary placeholder generated by standalone bridge.","ownerSummary":"Generic summary placeholder generated by standalone bridge."}'
194
+ `);
195
+ }
196
+
197
+ if (!fs.existsSync(notifyScript)) {
198
+ writeExecutableFile(notifyScript, `#!/usr/bin/env bash
199
+ set -euo pipefail
200
+ payload="$(cat || true)"
201
+ echo "a2a notify: $payload" >&2
202
+ `);
203
+ }
204
+
205
+ let generatedAdminToken = null;
206
+ if (!process.env.A2A_ADMIN_TOKEN) {
207
+ generatedAdminToken = crypto.randomBytes(24).toString('base64url');
208
+ }
209
+
210
+ return {
211
+ configDir,
212
+ configFile,
213
+ manifestFile,
214
+ bridgeDir,
215
+ turnScript,
216
+ summaryScript,
217
+ notifyScript,
218
+ generatedAdminToken
219
+ };
220
+ }
221
+
114
222
  const DASHBOARD_PLUGIN_MANIFEST = {
115
223
  id: DASHBOARD_PLUGIN_ID,
116
224
  name: 'A2A Dashboard Proxy',
@@ -378,20 +486,30 @@ a2a server --port 3001
378
486
  `;
379
487
 
380
488
  function install() {
381
- log('Installing A2A Calling for OpenClaw...\n');
489
+ log('Installing A2A Calling...\n');
382
490
 
383
491
  const hostname = flags.hostname || process.env.HOSTNAME || 'localhost';
384
492
  const port = String(flags.port || process.env.A2A_PORT || '3001');
385
493
  const backendUrl = flags['dashboard-backend'] || `http://127.0.0.1:${port}`;
386
-
387
- // 1. Create skills directory if needed
388
- ensureDir(OPENCLAW_SKILLS);
389
-
390
- // 2. Install skill
391
- const skillDir = path.join(OPENCLAW_SKILLS, SKILL_NAME);
392
- ensureDir(skillDir);
393
- fs.writeFileSync(path.join(skillDir, 'SKILL.md'), SKILL_MD);
394
- log(`Installed skill to: ${skillDir}`);
494
+ const forceStandalone = Boolean(flags.standalone) || String(process.env.A2A_FORCE_STANDALONE || '').toLowerCase() === 'true';
495
+ const hasOpenClawBinary = commandExists('openclaw');
496
+ const hasOpenClawConfig = fs.existsSync(OPENCLAW_CONFIG);
497
+ const hasOpenClaw = !forceStandalone && (hasOpenClawBinary || hasOpenClawConfig);
498
+ let standaloneBootstrap = null;
499
+
500
+ if (hasOpenClaw) {
501
+ // 1. Create skills directory if needed
502
+ ensureDir(OPENCLAW_SKILLS);
503
+
504
+ // 2. Install skill
505
+ const skillDir = path.join(OPENCLAW_SKILLS, SKILL_NAME);
506
+ ensureDir(skillDir);
507
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), SKILL_MD);
508
+ log(`Installed skill to: ${skillDir}`);
509
+ } else {
510
+ warn('OpenClaw not detected. Enabling standalone A2A bootstrap.');
511
+ standaloneBootstrap = ensureStandaloneBootstrap(hostname, port);
512
+ }
395
513
 
396
514
  // 3. Update OpenClaw config + gateway plugin setup (if available)
397
515
  let config = loadOpenClawConfig();
@@ -450,6 +568,12 @@ function install() {
450
568
  warn('Skipping OpenClaw command/plugin config updates');
451
569
  }
452
570
 
571
+ const runtimeLine = forceStandalone
572
+ ? 'Runtime forced to standalone mode for this setup run.'
573
+ : hasOpenClawBinary
574
+ ? 'Runtime auto-selects OpenClaw when available and falls back to generic if needed.'
575
+ : 'Runtime defaults to generic fallback (no OpenClaw dependency required).';
576
+
453
577
  console.log(`
454
578
  ${bold('━━━ Server Setup ━━━')}
455
579
 
@@ -466,6 +590,25 @@ ${dashboardMode === 'gateway'
466
590
  ? `Gateway path /a2a is now proxied to ${backendUrl}.`
467
591
  : 'No gateway detected. Dashboard is served directly from the A2A server.'}
468
592
 
593
+ ${bold('━━━ Runtime Setup ━━━')}
594
+
595
+ ${runtimeLine}
596
+ ${standaloneBootstrap
597
+ ? `Standalone bridge templates:
598
+ ${green(standaloneBootstrap.turnScript)}
599
+ ${green(standaloneBootstrap.summaryScript)}
600
+ ${green(standaloneBootstrap.notifyScript)}
601
+
602
+ Optional bridge wiring:
603
+ export A2A_RUNTIME=generic
604
+ export A2A_AGENT_COMMAND="${standaloneBootstrap.turnScript}"
605
+ export A2A_SUMMARY_COMMAND="${standaloneBootstrap.summaryScript}"
606
+ export A2A_NOTIFY_COMMAND="${standaloneBootstrap.notifyScript}"
607
+ ${standaloneBootstrap.generatedAdminToken ? `
608
+ Suggested dashboard admin token (set in env, do not commit):
609
+ export A2A_ADMIN_TOKEN="${standaloneBootstrap.generatedAdminToken}"` : ''}`
610
+ : 'No standalone bridge templates were created because OpenClaw was detected.'}
611
+
469
612
  ${bold('━━━ Usage ━━━')}
470
613
 
471
614
  In your chat app, use:
@@ -506,11 +649,11 @@ function uninstall() {
506
649
 
507
650
  function showHelp() {
508
651
  console.log(`
509
- ${bold('A2A Calling - OpenClaw Integration')}
652
+ ${bold('A2A Calling Setup')}
510
653
 
511
654
  Usage:
512
- npx a2acalling install [options] Install A2A for OpenClaw
513
- npx a2acalling setup [options] Alias for install (auto gateway/standalone)
655
+ npx a2acalling install [options] Install A2A (OpenClaw-aware + standalone)
656
+ npx a2acalling setup [options] Alias for install (auto runtime detection)
514
657
  npx a2acalling uninstall Remove A2A skill + dashboard plugin
515
658
  npx a2acalling server Start A2A server
516
659
 
@@ -519,10 +662,12 @@ Install Options:
519
662
  --port <port> A2A server port (default: 3001)
520
663
  --gateway-url <url> Force gateway base URL for printed dashboard link
521
664
  --dashboard-backend <url> Backend URL used by gateway dashboard proxy
665
+ --standalone Force standalone bootstrap (ignore OpenClaw detection)
522
666
 
523
667
  Examples:
524
668
  npx a2acalling install --hostname myserver.com --port 3001
525
669
  npx a2acalling setup --dashboard-backend http://127.0.0.1:3001
670
+ npx a2acalling setup --standalone
526
671
  npx a2acalling uninstall
527
672
  `);
528
673
  }
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ const { TokenStore } = require('./lib/tokens');
18
18
  const { A2AClient, A2AError } = require('./lib/client');
19
19
  const { createRoutes } = require('./routes/a2a');
20
20
  const { createDashboardApiRouter, createDashboardUiRouter } = require('./routes/dashboard');
21
+ const { createRuntimeAdapter, resolveRuntimeMode } = require('./lib/runtime-adapter');
21
22
 
22
23
  // Lazy load optional dependencies
23
24
  let ConversationStore = null;
@@ -48,6 +49,10 @@ module.exports = {
48
49
  // Dashboard routes
49
50
  createDashboardApiRouter,
50
51
  createDashboardUiRouter,
52
+
53
+ // Runtime adapter
54
+ createRuntimeAdapter,
55
+ resolveRuntimeMode,
51
56
 
52
57
  // Conversation storage (requires better-sqlite3)
53
58
  ConversationStore,
package/src/lib/config.js CHANGED
@@ -112,8 +112,13 @@ class A2AConfig {
112
112
  this.config.createdAt = this.config.updatedAt;
113
113
  }
114
114
  const tmpPath = `${CONFIG_FILE}.tmp`;
115
- fs.writeFileSync(tmpPath, JSON.stringify(this.config, null, 2));
115
+ fs.writeFileSync(tmpPath, JSON.stringify(this.config, null, 2), { mode: 0o600 });
116
116
  fs.renameSync(tmpPath, CONFIG_FILE);
117
+ try {
118
+ fs.chmodSync(CONFIG_FILE, 0o600);
119
+ } catch (err) {
120
+ // Best effort - ignore on platforms without chmod support.
121
+ }
117
122
  }
118
123
 
119
124
  // Check if onboarding is complete
@@ -39,6 +39,11 @@ class ConversationStore {
39
39
  try {
40
40
  const Database = require('better-sqlite3');
41
41
  this.db = new Database(this.dbPath);
42
+ try {
43
+ fs.chmodSync(this.dbPath, 0o600);
44
+ } catch (err) {
45
+ // Best effort - ignore on platforms without chmod support.
46
+ }
42
47
  this._migrate();
43
48
  return this.db;
44
49
  } catch (err) {
@@ -39,8 +39,13 @@ function saveManifest(manifest) {
39
39
  }
40
40
  manifest.updated_at = new Date().toISOString();
41
41
  const tmpPath = `${MANIFEST_FILE}.tmp`;
42
- fs.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2));
42
+ fs.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2), { mode: 0o600 });
43
43
  fs.renameSync(tmpPath, MANIFEST_FILE);
44
+ try {
45
+ fs.chmodSync(MANIFEST_FILE, 0o600);
46
+ } catch (err) {
47
+ // Best effort - ignore on platforms without chmod support.
48
+ }
44
49
  }
45
50
 
46
51
  /**
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Runtime adapter for inbound A2A calls.
3
+ *
4
+ * Modes:
5
+ * - openclaw: uses `openclaw` CLI for turn handling, summaries, notifications
6
+ * - generic: platform-agnostic fallback that never hard-fails calls
7
+ *
8
+ * Selection:
9
+ * - A2A_RUNTIME=openclaw|generic|auto (default: auto)
10
+ * - auto picks openclaw if CLI exists, otherwise generic
11
+ *
12
+ * Generic bridge hooks:
13
+ * - A2A_AGENT_COMMAND command that receives JSON payload on stdin and returns text or JSON
14
+ * - A2A_SUMMARY_COMMAND command that receives JSON payload on stdin and returns summary text/JSON
15
+ * - A2A_NOTIFY_COMMAND command that receives JSON payload on stdin for owner notifications
16
+ */
17
+
18
+ const { execSync } = require('child_process');
19
+
20
+ function commandExists(command) {
21
+ try {
22
+ execSync(`command -v ${command}`, { stdio: 'ignore' });
23
+ return true;
24
+ } catch (err) {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function cleanText(value, maxLength = 300) {
30
+ return String(value || '')
31
+ .replace(/\s+/g, ' ')
32
+ .trim()
33
+ .slice(0, maxLength);
34
+ }
35
+
36
+ function toBool(value, fallback = true) {
37
+ if (value === undefined || value === null || value === '') {
38
+ return fallback;
39
+ }
40
+ const normalized = String(value).trim().toLowerCase();
41
+ return !(normalized === '0' || normalized === 'false' || normalized === 'no');
42
+ }
43
+
44
+ function resolveRuntimeMode() {
45
+ const requested = String(process.env.A2A_RUNTIME || 'auto').trim().toLowerCase();
46
+ const hasOpenClaw = commandExists('openclaw');
47
+
48
+ if (requested === 'generic') {
49
+ return {
50
+ mode: 'generic',
51
+ requested,
52
+ hasOpenClaw,
53
+ reason: 'A2A_RUNTIME=generic'
54
+ };
55
+ }
56
+
57
+ if (requested === 'openclaw') {
58
+ if (hasOpenClaw) {
59
+ return {
60
+ mode: 'openclaw',
61
+ requested,
62
+ hasOpenClaw,
63
+ reason: 'A2A_RUNTIME=openclaw'
64
+ };
65
+ }
66
+ return {
67
+ mode: 'generic',
68
+ requested,
69
+ hasOpenClaw,
70
+ warning: 'A2A_RUNTIME=openclaw but openclaw CLI not found, falling back to generic runtime',
71
+ reason: 'forced-openclaw-missing'
72
+ };
73
+ }
74
+
75
+ if (hasOpenClaw) {
76
+ return {
77
+ mode: 'openclaw',
78
+ requested: 'auto',
79
+ hasOpenClaw,
80
+ reason: 'openclaw CLI detected'
81
+ };
82
+ }
83
+
84
+ return {
85
+ mode: 'generic',
86
+ requested: 'auto',
87
+ hasOpenClaw,
88
+ reason: 'openclaw CLI not detected'
89
+ };
90
+ }
91
+
92
+ function normalizeOpenClawOutput(raw) {
93
+ const lines = String(raw || '')
94
+ .split('\n')
95
+ .filter(line => {
96
+ if (!line.trim()) return false;
97
+ if (line.includes('[telegram-topic-tracker]')) return false;
98
+ if (line.includes('Plugin registered')) return false;
99
+ return true;
100
+ });
101
+ return lines.join('\n').trim();
102
+ }
103
+
104
+ function parseCommandTextOutput(rawOutput, keys = ['response', 'text', 'message']) {
105
+ const output = String(rawOutput || '').trim();
106
+ if (!output) {
107
+ return '';
108
+ }
109
+
110
+ try {
111
+ const parsed = JSON.parse(output);
112
+ if (parsed && typeof parsed === 'object') {
113
+ for (const key of keys) {
114
+ if (typeof parsed[key] === 'string' && parsed[key].trim()) {
115
+ return parsed[key].trim();
116
+ }
117
+ }
118
+ }
119
+ } catch (err) {
120
+ // Plain text output is valid for bridge commands.
121
+ }
122
+
123
+ return output;
124
+ }
125
+
126
+ function parseSummaryOutput(rawOutput) {
127
+ const output = String(rawOutput || '').trim();
128
+ if (!output) {
129
+ return null;
130
+ }
131
+
132
+ try {
133
+ const parsed = JSON.parse(output);
134
+ if (parsed && typeof parsed === 'object') {
135
+ const summary = cleanText(parsed.summary || parsed.text || parsed.message, 1500);
136
+ return {
137
+ ...parsed,
138
+ summary: summary || null,
139
+ ownerSummary: cleanText(
140
+ parsed.ownerSummary || parsed.owner_summary || summary || '',
141
+ 1500
142
+ ) || null
143
+ };
144
+ }
145
+ } catch (err) {
146
+ // Plain text is also acceptable.
147
+ }
148
+
149
+ const summary = cleanText(output, 1500);
150
+ return {
151
+ summary,
152
+ ownerSummary: summary
153
+ };
154
+ }
155
+
156
+ function runCommand(command, payload, options = {}) {
157
+ const payloadJson = JSON.stringify(payload || {});
158
+ const timeoutMs = options.timeoutMs || 60000;
159
+ return execSync(command, {
160
+ encoding: 'utf8',
161
+ timeout: timeoutMs,
162
+ stdio: ['pipe', 'pipe', 'pipe'],
163
+ input: payloadJson,
164
+ cwd: options.cwd || process.cwd(),
165
+ env: {
166
+ ...process.env,
167
+ A2A_PAYLOAD_JSON: payloadJson
168
+ }
169
+ });
170
+ }
171
+
172
+ function escapeCliValue(value) {
173
+ return String(value || '')
174
+ .replace(/\\/g, '\\\\')
175
+ .replace(/"/g, '\\"')
176
+ .replace(/\n/g, '\\n')
177
+ .replace(/\r/g, '');
178
+ }
179
+
180
+ function buildFallbackResponse(message, context = {}, reason = null) {
181
+ const callerName = cleanText(context.callerName || context.caller?.name || 'caller');
182
+ const ownerName = cleanText(context.ownerName || 'the owner');
183
+ const allowedTopics = Array.isArray(context.allowedTopics)
184
+ ? context.allowedTopics.filter(Boolean).slice(0, 4)
185
+ : [];
186
+ const topicText = allowedTopics.length > 0
187
+ ? allowedTopics.join(', ')
188
+ : 'permitted topics';
189
+ const excerpt = cleanText(message, 220) || 'No message content provided.';
190
+
191
+ let prefix = `I am running in generic A2A mode for ${ownerName}.`;
192
+ if (reason) {
193
+ prefix = `I switched to generic fallback mode (${cleanText(reason, 120)}).`;
194
+ }
195
+
196
+ return `${prefix} I received from ${callerName}: "${excerpt}". ` +
197
+ `We can still work through concrete overlap on ${topicText} and line up actionable next steps. ` +
198
+ `What outcome should we target first?`;
199
+ }
200
+
201
+ function buildFallbackSummary(messages = [], callerInfo = {}, reason = null) {
202
+ const inbound = messages.filter(m => m.direction === 'inbound');
203
+ const outbound = messages.filter(m => m.direction !== 'inbound');
204
+ const caller = cleanText(callerInfo?.name || 'Unknown caller');
205
+ const lastInbound = inbound.length > 0
206
+ ? cleanText(inbound[inbound.length - 1].content, 220)
207
+ : '';
208
+
209
+ const summary = [
210
+ `Call concluded with ${caller}.`,
211
+ `Inbound turns: ${inbound.length}. Outbound turns: ${outbound.length}.`,
212
+ lastInbound ? `Latest caller request: "${lastInbound}".` : '',
213
+ reason ? `Summary mode: ${cleanText(reason, 140)}.` : 'Summary mode: generic fallback.'
214
+ ].filter(Boolean).join(' ');
215
+
216
+ return {
217
+ summary,
218
+ ownerSummary: summary,
219
+ relevance: 'unknown',
220
+ goalsTouched: [],
221
+ ownerActionItems: [],
222
+ callerActionItems: [],
223
+ jointActionItems: [],
224
+ collaborationOpportunity: {
225
+ found: false,
226
+ rationale: 'Generic fallback summary (no platform-specific summarizer configured)'
227
+ },
228
+ followUp: lastInbound
229
+ ? `Clarify the next concrete step for: ${lastInbound}`
230
+ : 'Ask both owners to confirm desired follow-up scope.',
231
+ notes: reason
232
+ ? `Fallback summary generated after runtime issue: ${cleanText(reason, 180)}`
233
+ : 'Fallback summary generated by generic runtime.'
234
+ };
235
+ }
236
+
237
+ function createRuntimeAdapter(options = {}) {
238
+ const workspaceDir = options.workspaceDir || process.cwd();
239
+ const modeInfo = resolveRuntimeMode();
240
+ const failoverEnabled = toBool(process.env.A2A_RUNTIME_FAILOVER, true);
241
+
242
+ const genericAgentCommand = process.env.A2A_AGENT_COMMAND || '';
243
+ const genericSummaryCommand = process.env.A2A_SUMMARY_COMMAND || '';
244
+ const genericNotifyCommand = process.env.A2A_NOTIFY_COMMAND || '';
245
+
246
+ async function runOpenClawTurn({ sessionId, prompt, timeoutMs }) {
247
+ const timeoutSeconds = Math.max(5, Math.min(300, Math.round((timeoutMs || 65000) / 1000)));
248
+ const escapedPrompt = escapeCliValue(prompt);
249
+ const output = execSync(
250
+ `openclaw agent --session-id "${sessionId}" --message "${escapedPrompt}" --timeout ${timeoutSeconds} 2>&1`,
251
+ {
252
+ encoding: 'utf8',
253
+ timeout: (timeoutMs || 65000) + 5000,
254
+ maxBuffer: 1024 * 1024,
255
+ cwd: workspaceDir,
256
+ env: { ...process.env, FORCE_COLOR: '0' }
257
+ }
258
+ );
259
+ return normalizeOpenClawOutput(output) || '[Sub-agent returned empty response]';
260
+ }
261
+
262
+ async function runOpenClawSummary({ sessionId, prompt, timeoutMs }) {
263
+ const timeoutSeconds = Math.max(5, Math.min(120, Math.round((timeoutMs || 35000) / 1000)));
264
+ const escapedPrompt = escapeCliValue(prompt);
265
+ const output = execSync(
266
+ `openclaw agent --session-id "${sessionId}" --message "${escapedPrompt}" --timeout ${timeoutSeconds} 2>&1`,
267
+ {
268
+ encoding: 'utf8',
269
+ timeout: (timeoutMs || 35000) + 5000,
270
+ cwd: workspaceDir,
271
+ env: { ...process.env, FORCE_COLOR: '0' }
272
+ }
273
+ );
274
+ const summaryText = cleanText(normalizeOpenClawOutput(output), 1500);
275
+ if (!summaryText) {
276
+ return null;
277
+ }
278
+ return {
279
+ summary: summaryText,
280
+ ownerSummary: summaryText
281
+ };
282
+ }
283
+
284
+ async function runOpenClawNotify({ callerName, callerOwner, message }) {
285
+ const notification = `🤝 **A2A Call**\nFrom: ${callerName}${callerOwner}\n> ${message.slice(0, 150)}...`;
286
+ execSync(
287
+ `openclaw message send --channel telegram --message "${escapeCliValue(notification)}"`,
288
+ { timeout: 10000, stdio: 'pipe' }
289
+ );
290
+ }
291
+
292
+ async function runGenericTurn({ message, caller, context, runtimeError }) {
293
+ const payload = {
294
+ mode: 'a2a-turn',
295
+ message,
296
+ caller: caller || {},
297
+ context: context || {}
298
+ };
299
+
300
+ if (genericAgentCommand) {
301
+ try {
302
+ const output = runCommand(genericAgentCommand, payload, {
303
+ timeoutMs: context?.timeoutMs || 65000
304
+ });
305
+ const text = parseCommandTextOutput(output);
306
+ if (text) {
307
+ return text;
308
+ }
309
+ } catch (err) {
310
+ runtimeError = err.message;
311
+ console.error(`[a2a] Generic agent command failed: ${err.message}`);
312
+ }
313
+ }
314
+
315
+ return buildFallbackResponse(message, {
316
+ caller,
317
+ callerName: caller?.name,
318
+ ownerName: context?.ownerName,
319
+ allowedTopics: context?.allowedTopics
320
+ }, runtimeError);
321
+ }
322
+
323
+ async function runGenericSummary({ messages, callerInfo, reason }) {
324
+ const payload = {
325
+ mode: 'a2a-summary',
326
+ messages,
327
+ caller: callerInfo || {}
328
+ };
329
+
330
+ if (genericSummaryCommand) {
331
+ try {
332
+ const output = runCommand(genericSummaryCommand, payload, { timeoutMs: 35000 });
333
+ const parsed = parseSummaryOutput(output);
334
+ if (parsed && parsed.summary) {
335
+ return parsed;
336
+ }
337
+ } catch (err) {
338
+ reason = err.message;
339
+ console.error(`[a2a] Generic summary command failed: ${err.message}`);
340
+ }
341
+ }
342
+
343
+ return buildFallbackSummary(messages, callerInfo, reason);
344
+ }
345
+
346
+ async function runGenericNotify(payload) {
347
+ if (!genericNotifyCommand) {
348
+ return;
349
+ }
350
+ try {
351
+ runCommand(genericNotifyCommand, payload, { timeoutMs: 10000 });
352
+ } catch (err) {
353
+ console.error(`[a2a] Generic notify command failed: ${err.message}`);
354
+ }
355
+ }
356
+
357
+ async function runTurn({ sessionId, prompt, message, caller, context = {}, timeoutMs }) {
358
+ if (modeInfo.mode !== 'openclaw') {
359
+ return runGenericTurn({ message, caller, context });
360
+ }
361
+
362
+ try {
363
+ return await runOpenClawTurn({ sessionId, prompt, timeoutMs });
364
+ } catch (err) {
365
+ if (!failoverEnabled) {
366
+ throw err;
367
+ }
368
+ console.error(`[a2a] OpenClaw runtime failed, switching to generic fallback: ${err.message}`);
369
+ return runGenericTurn({
370
+ message,
371
+ caller,
372
+ context,
373
+ runtimeError: `openclaw runtime unavailable: ${err.message}`
374
+ });
375
+ }
376
+ }
377
+
378
+ async function summarize({ sessionId, prompt, messages, callerInfo }) {
379
+ if (modeInfo.mode !== 'openclaw') {
380
+ return runGenericSummary({ messages, callerInfo });
381
+ }
382
+
383
+ try {
384
+ const result = await runOpenClawSummary({
385
+ sessionId,
386
+ prompt,
387
+ timeoutMs: 35000
388
+ });
389
+ if (result && result.summary) {
390
+ return result;
391
+ }
392
+ return runGenericSummary({
393
+ messages,
394
+ callerInfo,
395
+ reason: 'empty summary from openclaw runtime'
396
+ });
397
+ } catch (err) {
398
+ if (!failoverEnabled) {
399
+ throw err;
400
+ }
401
+ console.error(`[a2a] OpenClaw summary failed, using generic fallback: ${err.message}`);
402
+ return runGenericSummary({
403
+ messages,
404
+ callerInfo,
405
+ reason: `openclaw summary unavailable: ${err.message}`
406
+ });
407
+ }
408
+ }
409
+
410
+ async function notify({ level, token, caller, message, conversationId }) {
411
+ const payload = {
412
+ mode: 'a2a-notify',
413
+ level,
414
+ token: token || null,
415
+ caller: caller || null,
416
+ message,
417
+ conversationId
418
+ };
419
+
420
+ if (modeInfo.mode !== 'openclaw') {
421
+ return runGenericNotify(payload);
422
+ }
423
+
424
+ if (level !== 'all') {
425
+ return;
426
+ }
427
+
428
+ const callerName = caller?.name || 'Unknown';
429
+ const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
430
+
431
+ try {
432
+ await runOpenClawNotify({ callerName, callerOwner, message: message || '' });
433
+ } catch (err) {
434
+ if (!failoverEnabled) {
435
+ throw err;
436
+ }
437
+ console.error(`[a2a] OpenClaw notify failed, running generic notifier: ${err.message}`);
438
+ await runGenericNotify(payload);
439
+ }
440
+ }
441
+
442
+ return {
443
+ mode: modeInfo.mode,
444
+ requestedMode: modeInfo.requested,
445
+ hasOpenClaw: modeInfo.hasOpenClaw,
446
+ reason: modeInfo.reason,
447
+ warning: modeInfo.warning || null,
448
+ failoverEnabled,
449
+ runTurn,
450
+ summarize,
451
+ notify,
452
+ buildFallbackResponse
453
+ };
454
+ }
455
+
456
+ module.exports = {
457
+ createRuntimeAdapter,
458
+ resolveRuntimeMode,
459
+ buildFallbackResponse
460
+ };
package/src/lib/tokens.js CHANGED
@@ -44,8 +44,13 @@ class TokenStore {
44
44
  _save(db) {
45
45
  // Atomic write: write to temp file, then rename
46
46
  const tmpPath = `${this.dbPath}.tmp.${process.pid}`;
47
- fs.writeFileSync(tmpPath, JSON.stringify(db, null, 2));
47
+ fs.writeFileSync(tmpPath, JSON.stringify(db, null, 2), { mode: 0o600 });
48
48
  fs.renameSync(tmpPath, this.dbPath);
49
+ try {
50
+ fs.chmodSync(this.dbPath, 0o600);
51
+ } catch (err) {
52
+ // Best effort - ignore on platforms without chmod support.
53
+ }
49
54
  }
50
55
 
51
56
  /**
@@ -460,6 +465,78 @@ class TokenStore {
460
465
  this._save(db);
461
466
  return { success: true, remote: removed };
462
467
  }
468
+
469
+ /**
470
+ * Ensure an inbound caller is present in contacts.
471
+ *
472
+ * Inbound callers authenticate with a token we issued; we usually don't
473
+ * have their endpoint URL, so these records are "placeholders" with:
474
+ * - host: "inbound"
475
+ * - no token stored
476
+ * - linked_token_id: token id used to call us (tok_...)
477
+ *
478
+ * This allows mapping call history (SQLite contact_id=tok_...) to a contact
479
+ * row in the dashboard via linked_token_id.
480
+ */
481
+ ensureInboundContact(caller, tokenId) {
482
+ if (!caller || !caller.name) {
483
+ return null;
484
+ }
485
+
486
+ const name = String(caller.name || '').trim().slice(0, 120);
487
+ const owner = caller.owner ? String(caller.owner).trim().slice(0, 120) : null;
488
+
489
+ const db = this._load();
490
+ db.remotes = db.remotes || [];
491
+
492
+ // Prefer stable linking by the token used for inbound auth.
493
+ let remote = tokenId
494
+ ? db.remotes.find(r => r.linked_token_id === tokenId)
495
+ : null;
496
+
497
+ // Fallback match by agent name/owner (less reliable, but helpful).
498
+ if (!remote) {
499
+ remote = db.remotes.find(r => r.name === name || (owner && r.owner === owner));
500
+ }
501
+
502
+ if (remote) {
503
+ remote.name = remote.name || name;
504
+ if (owner && !remote.owner) {
505
+ remote.owner = owner;
506
+ }
507
+ if (tokenId && !remote.linked_token_id) {
508
+ remote.linked_token_id = tokenId;
509
+ }
510
+ remote.host = remote.host || 'inbound';
511
+ remote.tags = Array.isArray(remote.tags) ? remote.tags : [];
512
+ if (!remote.tags.includes('inbound')) {
513
+ remote.tags.push('inbound');
514
+ }
515
+ remote.status = remote.status || 'unknown';
516
+ remote.updated_at = new Date().toISOString();
517
+ this._save(db);
518
+ return remote;
519
+ }
520
+
521
+ const contact = {
522
+ id: crypto.randomBytes(8).toString('hex'),
523
+ name,
524
+ owner,
525
+ host: 'inbound',
526
+ token_hash: null,
527
+ token_enc: null,
528
+ notes: tokenId ? `Inbound caller via token ${tokenId}` : 'Inbound caller',
529
+ tags: ['inbound'],
530
+ linked_token_id: tokenId || null,
531
+ status: 'unknown',
532
+ last_seen: null,
533
+ added_at: new Date().toISOString()
534
+ };
535
+
536
+ db.remotes.push(contact);
537
+ this._save(db);
538
+ return contact;
539
+ }
463
540
  }
464
541
 
465
542
  // Legacy tier values from old records → label mapping
package/src/routes/a2a.js CHANGED
@@ -57,6 +57,14 @@ const MAX_MESSAGE_LENGTH = 10000; // 10KB max message
57
57
  const MAX_TIMEOUT_SECONDS = 300; // 5 min max timeout
58
58
  const MIN_TIMEOUT_SECONDS = 5; // 5 sec min timeout
59
59
 
60
+ function isLoopbackAddress(ip) {
61
+ if (!ip) return false;
62
+ if (ip === '::1' || ip === '127.0.0.1' || ip === '::ffff:127.0.0.1') {
63
+ return true;
64
+ }
65
+ return ip.startsWith('::ffff:127.');
66
+ }
67
+
60
68
  function checkRateLimit(tokenId, limits = { minute: 10, hour: 100, day: 1000 }) {
61
69
  const now = Date.now();
62
70
  const minute = Math.floor(now / 60000);
@@ -399,9 +407,18 @@ function createRoutes(options = {}) {
399
407
  router.get('/conversations', (req, res) => {
400
408
  // This endpoint should be protected by local auth, not A2A tokens
401
409
  // For now, require an admin token or local access
410
+ const expected = process.env.A2A_ADMIN_TOKEN;
402
411
  const adminToken = req.headers['x-admin-token'];
403
- if (adminToken !== process.env.A2A_ADMIN_TOKEN && req.ip !== '127.0.0.1') {
404
- return res.status(401).json({ error: 'unauthorized' });
412
+ if (!isLoopbackAddress(req.ip)) {
413
+ if (!expected) {
414
+ return res.status(401).json({
415
+ error: 'admin_token_required',
416
+ message: 'Set A2A_ADMIN_TOKEN to access conversation admin routes from non-local addresses'
417
+ });
418
+ }
419
+ if (adminToken !== expected) {
420
+ return res.status(401).json({ error: 'unauthorized' });
421
+ }
405
422
  }
406
423
 
407
424
  const convStore = getConversationStore();
@@ -426,9 +443,18 @@ function createRoutes(options = {}) {
426
443
  * Get conversation details with context
427
444
  */
428
445
  router.get('/conversations/:id', (req, res) => {
446
+ const expected = process.env.A2A_ADMIN_TOKEN;
429
447
  const adminToken = req.headers['x-admin-token'];
430
- if (adminToken !== process.env.A2A_ADMIN_TOKEN && req.ip !== '127.0.0.1') {
431
- return res.status(401).json({ error: 'unauthorized' });
448
+ if (!isLoopbackAddress(req.ip)) {
449
+ if (!expected) {
450
+ return res.status(401).json({
451
+ error: 'admin_token_required',
452
+ message: 'Set A2A_ADMIN_TOKEN to access conversation admin routes from non-local addresses'
453
+ });
454
+ }
455
+ if (adminToken !== expected) {
456
+ return res.status(401).json({ error: 'unauthorized' });
457
+ }
432
458
  }
433
459
 
434
460
  const convStore = getConversationStore();
@@ -169,7 +169,11 @@ function ensureDashboardAccess(req, res, next) {
169
169
  return next();
170
170
  }
171
171
  if (!adminToken) {
172
- return next();
172
+ return res.status(401).json({
173
+ success: false,
174
+ error: 'admin_token_required',
175
+ message: 'Set A2A_ADMIN_TOKEN to access dashboard from non-local addresses'
176
+ });
173
177
  }
174
178
  if (headerToken === adminToken || queryToken === adminToken) {
175
179
  return next();
package/src/server.js CHANGED
@@ -2,17 +2,17 @@
2
2
  /**
3
3
  * A2A Server
4
4
  *
5
- * Routes A2A calls to OpenClaw sub-agents.
5
+ * Routes A2A calls through a runtime adapter (OpenClaw or generic fallback).
6
6
  * Auto-adds contacts, generates summaries, notifies owner.
7
7
  */
8
8
 
9
9
  const express = require('express');
10
- const { execSync } = require('child_process');
11
10
  const fs = require('fs');
12
11
  const path = require('path');
13
12
  const { createRoutes } = require('./routes/a2a');
14
13
  const { createDashboardApiRouter, createDashboardUiRouter } = require('./routes/dashboard');
15
14
  const { TokenStore } = require('./lib/tokens');
15
+ const { createRuntimeAdapter } = require('./lib/runtime-adapter');
16
16
  const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./lib/disclosure');
17
17
  const {
18
18
  buildConnectionPrompt,
@@ -21,16 +21,23 @@ const {
21
21
  } = require('./lib/prompt-template');
22
22
 
23
23
  const port = process.env.PORT || parseInt(process.argv[2]) || 3001;
24
- const workspaceDir = process.env.OPENCLAW_WORKSPACE || '/root/clawd';
24
+ const workspaceDir = process.env.A2A_WORKSPACE || process.env.OPENCLAW_WORKSPACE || process.cwd();
25
25
 
26
26
  // Load workspace context for agent identity
27
27
  function loadAgentContext() {
28
- let context = { name: 'bappybot', owner: 'Ben Pollack' };
28
+ let context = {
29
+ name: process.env.A2A_AGENT_NAME || process.env.AGENT_NAME || 'a2a-agent',
30
+ owner: process.env.A2A_OWNER_NAME || process.env.USER || 'Agent Owner'
31
+ };
29
32
 
30
33
  try {
31
34
  const userPath = path.join(workspaceDir, 'USER.md');
32
35
  if (fs.existsSync(userPath)) {
33
36
  const content = fs.readFileSync(userPath, 'utf8');
37
+ const agentMatch = content.match(/\*\*Agent:\*\*\s*([^\n]+)/i);
38
+ if (agentMatch && agentMatch[1]) {
39
+ context.name = agentMatch[1].trim().slice(0, 80) || context.name;
40
+ }
34
41
  const nameMatch = content.match(/\*\*Name:\*\*\s*([^\n]+)/);
35
42
  if (nameMatch) {
36
43
  const name = nameMatch[1].trim();
@@ -46,12 +53,20 @@ function loadAgentContext() {
46
53
 
47
54
  const agentContext = loadAgentContext();
48
55
  const tokenStore = new TokenStore();
56
+ const runtime = createRuntimeAdapter({
57
+ workspaceDir,
58
+ agentContext
59
+ });
49
60
  const VALID_PHASES = new Set(['handshake', 'explore', 'deep_dive', 'synthesize', 'close']);
50
61
  const collaborationSessions = new Map();
51
62
  const COLLAB_STATE_TTL_MS = readPositiveIntEnv('A2A_COLLAB_STATE_TTL_MS', 6 * 60 * 60 * 1000);
52
63
  const MAX_COLLAB_SESSIONS = readPositiveIntEnv('A2A_COLLAB_MAX_SESSIONS', 500);
53
64
 
54
65
  console.log(`[a2a] Agent: ${agentContext.name} (${agentContext.owner}'s agent)`);
66
+ console.log(`[a2a] Runtime: ${runtime.mode} (${runtime.reason})`);
67
+ if (runtime.warning) {
68
+ console.warn(`[a2a] Runtime warning: ${runtime.warning}`);
69
+ }
55
70
 
56
71
  function readPositiveIntEnv(name, fallback) {
57
72
  const parsed = Number.parseInt(process.env[name] || '', 10);
@@ -382,42 +397,15 @@ function fallbackCollaborationUpdate(state, inboundMessage, responseText, tierTo
382
397
  */
383
398
  function ensureContact(caller, tokenId) {
384
399
  if (!caller?.name) return null;
385
-
400
+
386
401
  try {
387
- const remotes = tokenStore.listRemotes();
388
- const existing = remotes.find(r =>
389
- r.name === caller.name ||
390
- (caller.owner && r.owner === caller.owner)
391
- );
392
-
393
- if (existing) {
394
- return existing;
402
+ const contact = tokenStore.ensureInboundContact(caller, tokenId);
403
+ if (contact) {
404
+ console.log(`[a2a] 📇 Contact ensured: ${caller.name}${caller.owner ? ` (${caller.owner})` : ''}`);
395
405
  }
396
-
397
- // Create a placeholder contact for the caller
398
- const contact = {
399
- id: `contact_${Date.now()}`,
400
- name: caller.name,
401
- owner: caller.owner || null,
402
- host: 'inbound', // They called us, we don't have their URL
403
- added_at: new Date().toISOString(),
404
- notes: `Inbound caller via token ${tokenId}`,
405
- tags: ['inbound'],
406
- status: 'unknown',
407
- linkedTokenId: tokenId
408
- };
409
-
410
- // Save to remotes
411
- const db = JSON.parse(fs.readFileSync(tokenStore.dbPath, 'utf8'));
412
- db.remotes = db.remotes || [];
413
- db.remotes.push(contact);
414
- fs.writeFileSync(tokenStore.dbPath, JSON.stringify(db, null, 2));
415
-
416
- console.log(`[a2a] 📇 New contact added: ${caller.name}${caller.owner ? ` (${caller.owner})` : ''}`);
417
406
  return contact;
418
-
419
407
  } catch (err) {
420
- console.error('[a2a] Failed to add contact:', err.message);
408
+ console.error('[a2a] Failed to ensure contact:', err.message);
421
409
  return null;
422
410
  }
423
411
  }
@@ -489,30 +477,19 @@ async function callAgent(message, a2aContext) {
489
477
  const sessionId = `a2a-${conversationId}`;
490
478
 
491
479
  try {
492
- const escapedPrompt = prompt
493
- .replace(/\\/g, '\\\\')
494
- .replace(/"/g, '\\"')
495
- .replace(/\n/g, '\\n')
496
- .replace(/\r/g, '');
497
-
498
- const result = execSync(
499
- `openclaw agent --session-id "${sessionId}" --message "${escapedPrompt}" --timeout 55 2>&1`,
500
- {
501
- encoding: 'utf8',
502
- timeout: 65000,
503
- maxBuffer: 1024 * 1024,
504
- cwd: workspaceDir,
505
- env: { ...process.env, FORCE_COLOR: '0' }
480
+ const rawResponse = await runtime.runTurn({
481
+ sessionId,
482
+ prompt,
483
+ message,
484
+ caller: a2aContext.caller || {},
485
+ timeoutMs: 65000,
486
+ context: {
487
+ conversationId,
488
+ tier: tierInfo,
489
+ ownerName: agentContext.owner,
490
+ allowedTopics: a2aContext.allowed_topics || []
506
491
  }
507
- );
508
-
509
- const lines = result.split('\n').filter(line =>
510
- !line.includes('[telegram-topic-tracker]') &&
511
- !line.includes('Plugin registered') &&
512
- line.trim()
513
- );
514
-
515
- const rawResponse = lines.join('\n').trim() || '[Sub-agent returned empty response]';
492
+ });
516
493
 
517
494
  if (collabMode !== 'adaptive') {
518
495
  return rawResponse;
@@ -549,8 +526,12 @@ async function callAgent(message, a2aContext) {
549
526
  return cleanResponse || '[Sub-agent returned empty response]';
550
527
 
551
528
  } catch (err) {
552
- console.error('[a2a] Sub-agent spawn failed:', err.message);
553
- return `[Sub-agent error: ${err.message}]`;
529
+ console.error('[a2a] Runtime turn handling failed:', err.message);
530
+ return runtime.buildFallbackResponse(message, {
531
+ caller: a2aContext.caller,
532
+ ownerName: agentContext.owner,
533
+ allowedTopics: a2aContext.allowed_topics || []
534
+ }, err.message);
554
535
  }
555
536
  }
556
537
 
@@ -584,25 +565,12 @@ Structure your summary with these sections:
584
565
  Be concise but specific. No filler.`;
585
566
 
586
567
  try {
587
- const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n');
588
- const result = execSync(
589
- `openclaw agent --session-id "summary-${Date.now()}" --message "${escapedPrompt}" --timeout 30 2>&1`,
590
- { encoding: 'utf8', timeout: 35000, cwd: workspaceDir, env: { ...process.env, FORCE_COLOR: '0' } }
591
- );
592
-
593
- // Filter out noise and get the summary
594
- const lines = result.split('\n').filter(line =>
595
- !line.includes('[telegram-topic-tracker]') &&
596
- !line.includes('Plugin registered') &&
597
- line.trim()
598
- );
599
-
600
- const summaryText = lines.join(' ').trim().slice(0, 1000);
601
-
602
- return {
603
- summary: summaryText,
604
- ownerSummary: summaryText
605
- };
568
+ return await runtime.summarize({
569
+ sessionId: `summary-${Date.now()}`,
570
+ prompt,
571
+ messages,
572
+ callerInfo
573
+ });
606
574
  } catch (err) {
607
575
  console.error('[a2a] Summary generation failed:', err.message);
608
576
  return null;
@@ -615,22 +583,21 @@ Be concise but specific. No filler.`;
615
583
  async function notifyOwner({ level, token, caller, message, conversation_id }) {
616
584
  const callerName = caller?.name || 'Unknown';
617
585
  const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
586
+ const messageText = String(message || '');
618
587
 
619
588
  console.log(`[a2a] 📞 Call from ${callerName}${callerOwner}`);
620
589
  console.log(`[a2a] Token: ${token?.name || 'unknown'}`);
621
- console.log(`[a2a] Message: ${message.slice(0, 100)}...`);
622
-
623
- // Try to notify via Telegram
624
- if (level === 'all') {
625
- try {
626
- const notification = `🤝 **A2A Call**\nFrom: ${callerName}${callerOwner}\n> ${message.slice(0, 150)}...`;
627
- execSync(`openclaw message send --channel telegram --message "${notification.replace(/"/g, '\\"')}"`, {
628
- timeout: 10000, stdio: 'pipe'
629
- });
630
- } catch (e) {
631
- // Notification failed, continue anyway
632
- }
590
+ if (messageText) {
591
+ console.log(`[a2a] Message: ${messageText.slice(0, 100)}...`);
633
592
  }
593
+
594
+ await runtime.notify({
595
+ level,
596
+ token,
597
+ caller,
598
+ message: messageText,
599
+ conversationId: conversation_id
600
+ });
634
601
  }
635
602
 
636
603
  const app = express();
@@ -668,6 +635,7 @@ app.get('/', (req, res) => {
668
635
  app.listen(port, () => {
669
636
  console.log(`[a2a] A2A server listening on port ${port}`);
670
637
  console.log(`[a2a] Agent: ${agentContext.name} - LIVE`);
638
+ console.log(`[a2a] Runtime mode: ${runtime.mode}${runtime.failoverEnabled ? ' (failover enabled)' : ''}`);
671
639
  console.log(`[a2a] Collaboration mode: ${resolveCollabMode()}`);
672
- console.log(`[a2a] Features: sub-agents, auto-contacts, summaries`);
640
+ console.log(`[a2a] Features: adaptive collaboration, auto-contacts, summaries, dashboard`);
673
641
  });