a2acalling 0.6.0 → 0.6.2

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/docs/protocol.md CHANGED
@@ -153,13 +153,13 @@ Dashboard API endpoints:
153
153
 
154
154
  Example filters for `/logs`: `trace_id`, `conversation_id`, `token_id`, `error_code`, `status_code`, `component`, `event`, `level`, `search`, `from`, `to`.
155
155
 
156
- ## Permission Scopes
156
+ ## Permission Tiers
157
157
 
158
- | Scope | Tools | Files | Memory | Actions |
159
- |-------|-------|-------|--------|---------|
160
- | `chat-only` | | ❌ | ❌ | ❌ |
161
- | `tools-read` | Read | Read | | ❌ |
162
- | `tools-write` | All | All | | Notify owner |
158
+ | Tier | Default capabilities |
159
+ |------|----------------------|
160
+ | `public` | `context-read` |
161
+ | `friends` | `context-read`, `calendar.read`, `email.read`, `search` |
162
+ | `family` | `context-read`, `calendar`, `email`, `search`, `tools`, `memory` |
163
163
 
164
164
  ## Disclosure Levels
165
165
 
@@ -177,10 +177,14 @@ Stored in `~/.config/openclaw/a2a.json`:
177
177
  {
178
178
  "tokens": [
179
179
  {
180
- "id": "fed_abc123xyz789",
180
+ "id": "tok_abc123xyz789",
181
181
  "token_hash": "sha256...",
182
182
  "name": "Alice's agent",
183
- "permissions": "chat-only",
183
+ "tier": "public",
184
+ "capabilities": ["context-read"],
185
+ "allowed_topics": ["chat"],
186
+ "allowed_goals": [],
187
+ "tier_settings": {},
184
188
  "disclosure": "minimal",
185
189
  "notify": "all",
186
190
  "max_calls": null,
@@ -191,16 +195,22 @@ Stored in `~/.config/openclaw/a2a.json`:
191
195
  "revoked": false
192
196
  }
193
197
  ],
194
- "remotes": [
198
+ "contacts": [
195
199
  {
196
- "id": "remote_xyz",
200
+ "id": "contact_xyz",
197
201
  "name": "Bob's agent",
202
+ "owner": "Bob",
198
203
  "host": "bob.example.com",
199
- "token": "fed_bobtoken123",
204
+ "token_hash": "sha256...",
205
+ "token_enc": "base64...",
206
+ "server_name": "Bob's server",
207
+ "notes": "Met via A2A",
208
+ "tags": ["collaborator"],
209
+ "fields": { "email": "bob@example.com" },
210
+ "linked_token_id": "tok_abc123xyz789",
200
211
  "added_at": "2026-02-11T18:00:00Z"
201
212
  }
202
- ],
203
- "calls": []
213
+ ]
204
214
  }
205
215
  ```
206
216
 
@@ -278,7 +288,7 @@ When handling an A2A call, inject context:
278
288
  "a2a": {
279
289
  "active": true,
280
290
  "caller": "Alice's Agent",
281
- "permissions": "chat-only",
291
+ "tier": "public",
282
292
  "disclosure": "minimal"
283
293
  }
284
294
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -79,53 +79,6 @@ function loadOpenClawConfig() {
79
79
  }
80
80
  }
81
81
 
82
- function normalizeDashboardPluginEntry(rawEntry, backendUrl) {
83
- const issues = [];
84
- const normalized = {
85
- enabled: true,
86
- config: {}
87
- };
88
- const entry = (rawEntry && typeof rawEntry === 'object') ? rawEntry : {};
89
-
90
- const legacyBackendUrl = (typeof entry.backendUrl === 'string' && entry.backendUrl.trim())
91
- ? entry.backendUrl.trim()
92
- : null;
93
- const rawConfig = (entry.config && typeof entry.config === 'object') ? entry.config : null;
94
-
95
- if (!entry || typeof entry !== 'object') {
96
- issues.push('plugin entry is missing or invalid');
97
- }
98
-
99
- if (entry && typeof entry.enabled === 'boolean') {
100
- normalized.enabled = entry.enabled;
101
- }
102
-
103
- if (rawConfig) {
104
- normalized.config = { ...rawConfig };
105
- } else if (entry.config !== undefined) {
106
- issues.push('plugin entry has non-object config; replacing with empty object');
107
- }
108
-
109
- if (legacyBackendUrl) {
110
- issues.push(`legacy key detected: plugins.entries.${DASHBOARD_PLUGIN_ID}.backendUrl (using backendUrl migration)`);
111
- normalized.config.backendUrl = backendUrl || legacyBackendUrl;
112
- } else if (typeof normalized.config.backendUrl === 'string' && normalized.config.backendUrl.trim()) {
113
- normalized.config.backendUrl = normalized.config.backendUrl.trim();
114
- } else if (typeof backendUrl === 'string' && backendUrl.trim()) {
115
- normalized.config.backendUrl = backendUrl.trim();
116
- } else {
117
- issues.push('backendUrl could not be determined; plugin may fail to route dashboard traffic');
118
- }
119
-
120
- return {
121
- normalized,
122
- issues,
123
- changed: issues.length > 0,
124
- legacyBackendUrl,
125
- summary: `a2a-dashboard-proxy config => ${normalized.enabled ? 'enabled' : 'disabled'}, backendUrl=${normalized.config.backendUrl || 'missing'}`
126
- };
127
- }
128
-
129
82
  function writeOpenClawConfig(config) {
130
83
  const backupPath = `${OPENCLAW_CONFIG}.backup.${Date.now()}`;
131
84
  fs.copyFileSync(OPENCLAW_CONFIG, backupPath);
@@ -135,9 +88,12 @@ function writeOpenClawConfig(config) {
135
88
  }
136
89
 
137
90
  function detectGateway(config) {
138
- const hasBinary = commandExists('openclaw');
139
91
  const hasGatewayBlock = Boolean(config?.gateway);
140
- return hasBinary && hasGatewayBlock;
92
+ // Treat the presence of a gateway block in config as "gateway mode" even if
93
+ // the `openclaw` binary is not on PATH (e.g., CI, remote provisioning, or
94
+ // minimal environments). We still want to migrate config + install the proxy
95
+ // plugin in those cases.
96
+ return hasGatewayBlock;
141
97
  }
142
98
 
143
99
  function resolveGatewayBaseUrl() {
@@ -345,8 +301,6 @@ function resolveBackendUrl(api: OpenClawPluginApi): URL {
345
301
  const entryConfig = (pluginEntry.config || {}) as Record<string, unknown>;
346
302
  const candidate = typeof entryConfig.backendUrl === "string" && entryConfig.backendUrl
347
303
  ? entryConfig.backendUrl
348
- : typeof pluginEntry.backendUrl === "string" && pluginEntry.backendUrl
349
- ? pluginEntry.backendUrl
350
304
  : fallback;
351
305
  return new URL(candidate);
352
306
  } catch {
@@ -494,10 +448,6 @@ function readExistingConfiguredInviteHost() {
494
448
  if (!parsed.hostname || isLocalOrUnroutableHost(parsed.hostname)) {
495
449
  return '';
496
450
  }
497
- // Legacy: Cloudflare quick tunnels are ephemeral and no longer supported.
498
- if (String(parsed.hostname).toLowerCase().endsWith('.trycloudflare.com')) {
499
- return '';
500
- }
501
451
  return existing;
502
452
  } catch (err) {
503
453
  return '';
@@ -703,6 +653,27 @@ async function install() {
703
653
  warn(w);
704
654
  }
705
655
 
656
+ // Network diagnostics: fetch the current egress IP via the same mechanism used for invite host resolution.
657
+ // This is extremely useful during install to separate "networking/ingress" issues from "code" issues.
658
+ let externalIpProbe = null;
659
+ try {
660
+ const { getExternalIp } = require('../src/lib/external-ip');
661
+ // Force refresh so we can surface "blocked outbound HTTPS/DNS" problems during setup.
662
+ externalIpProbe = await getExternalIp({ timeoutMs: 2500, forceRefresh: true });
663
+ } catch (err) {
664
+ externalIpProbe = {
665
+ ip: null,
666
+ error: err && err.message ? err.message : 'external_ip_probe_failed'
667
+ };
668
+ }
669
+ const externalIpSummary = (externalIpProbe && externalIpProbe.ip)
670
+ ? `${externalIpProbe.ip}${externalIpProbe.source ? ` (source: ${externalIpProbe.source})` : ''}` +
671
+ `${externalIpProbe.fromCache ? ' [cache]' : ''}${externalIpProbe.stale ? ' [stale]' : ''}`
672
+ : `FAILED${externalIpProbe && externalIpProbe.error ? ` (${externalIpProbe.error})` : ''}`;
673
+ const externalIpAttempts = (externalIpProbe && Array.isArray(externalIpProbe.attempts))
674
+ ? externalIpProbe.attempts
675
+ : [];
676
+
706
677
  const forceStandalone = Boolean(flags.standalone) || String(process.env.A2A_FORCE_STANDALONE || '').toLowerCase() === 'true';
707
678
  const hasOpenClawBinary = commandExists('openclaw');
708
679
  const hasOpenClawConfig = fs.existsSync(OPENCLAW_CONFIG);
@@ -775,17 +746,15 @@ async function install() {
775
746
  config.plugins = config.plugins || {};
776
747
  config.plugins.entries = config.plugins.entries || {};
777
748
  const rawEntry = config.plugins.entries[DASHBOARD_PLUGIN_ID];
778
- const audit = normalizeDashboardPluginEntry(rawEntry, backendUrl);
779
- for (const issue of audit.issues) {
780
- warn(`a2a-dashboard-proxy config issue: ${issue}`);
781
- }
782
- if (audit.legacyBackendUrl) {
783
- warn(`Auto-fixing legacy key: plugins.entries.${DASHBOARD_PLUGIN_ID}.backendUrl`);
784
- }
785
- if (audit.changed) {
786
- log(`Migrated dashboard plugin config: ${audit.summary}`);
787
- }
788
- config.plugins.entries[DASHBOARD_PLUGIN_ID] = audit.normalized;
749
+ const existingEnabled = (rawEntry && typeof rawEntry.enabled === 'boolean') ? rawEntry.enabled : true;
750
+ const existingConfig = (rawEntry && rawEntry.config && typeof rawEntry.config === 'object') ? rawEntry.config : {};
751
+ config.plugins.entries[DASHBOARD_PLUGIN_ID] = {
752
+ enabled: existingEnabled,
753
+ config: {
754
+ ...existingConfig,
755
+ backendUrl
756
+ }
757
+ };
789
758
  configUpdated = true;
790
759
  log(`Configured gateway plugin entry: ${DASHBOARD_PLUGIN_ID}`);
791
760
  }
@@ -833,6 +802,21 @@ ${bold('━━━ Ingress Setup ━━━')}
833
802
  Invite host: ${green(inviteHost)}
834
803
  Expected ping URL: ${green(invitePingUrl)}
835
804
 
805
+ External IP probe (egress):
806
+ ${externalIpProbe && externalIpProbe.source && String(externalIpProbe.source).includes('ifconfig.me')
807
+ ? green('ifconfig.me')
808
+ : 'external-ip resolver'} → ${externalIpProbe && externalIpProbe.ip ? green(externalIpSummary) : yellow(externalIpSummary)}
809
+ ${externalIpAttempts.length
810
+ ? ` Attempts:
811
+ ${externalIpAttempts.map(a => {
812
+ const service = a && a.service ? String(a.service) : '-';
813
+ const ok = Boolean(a && a.ok);
814
+ const status = a && a.statusCode ? ` (${a.statusCode})` : '';
815
+ const err = a && a.error ? ` (${a.error})` : '';
816
+ return ` - ${service}: ${ok ? 'ok' + status : 'failed' + err}`;
817
+ }).join('\n')}`
818
+ : ''}
819
+
836
820
  Port 80 scan:
837
821
  ${port80.a2aPingOk
838
822
  ? green('Port 80 responds to /api/a2a/ping (A2A ready on :80)')
@@ -876,6 +860,18 @@ ${inviteLooksLocal
876
860
  ? green(`External ping OK via ${externalPing.provider}`)
877
861
  : yellow(`External ping FAILED (expected if the server is not running yet, or ingress is not publicly reachable).`)}
878
862
 
863
+ ${bold('━━━ Debug Quick Checks ━━━')}
864
+
865
+ On the server:
866
+ ${green(`curl -s http://127.0.0.1:${backendPort}/api/a2a/status`)}
867
+ ${green('curl -s https://ifconfig.me/ip')}
868
+
869
+ From a remote machine (tests ingress):
870
+ ${green(`curl -s ${inviteScheme}://${inviteHost}/api/a2a/status`)}
871
+
872
+ If calls fail, check the dashboard logs:
873
+ Open ${green(dashboardUrl)} -> Logs
874
+
879
875
  ${bold('━━━ Dashboard Setup ━━━')}
880
876
 
881
877
  Mode: ${dashboardMode === 'gateway' ? green('gateway') : yellow('standalone')}
@@ -912,8 +908,8 @@ In your chat app, use:
912
908
  /a2a invite Create an invitation token
913
909
  /a2a list List active tokens
914
910
  /a2a revoke <id> Revoke a token
915
- /a2a add <url> Add a remote agent
916
- /a2a call <url> <msg> Call a remote agent
911
+ /a2a add <url> Add a contact
912
+ /a2a call <url> <msg> Call a contact
917
913
 
918
914
  ${bold('━━━ Done! ━━━')}
919
915