a2acalling 0.6.51 → 0.6.53

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
@@ -149,6 +149,7 @@ Customize tiers in `~/.config/openclaw/a2a-config.json`:
149
149
  "tiers": {
150
150
  "friends": {
151
151
  "topics": ["chat", "web", "files", "calendar"],
152
+ "allowed_tools": ["Bash(readonly)", "Read", "Grep", "Glob", "WebSearch", "WebFetch"],
152
153
  "disclosure": "minimal"
153
154
  }
154
155
  }
package/bin/cli.js CHANGED
@@ -646,6 +646,22 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
646
646
  return tierData.topics.map(t => String(t && t.topic || '').trim()).filter(Boolean);
647
647
  }
648
648
 
649
+ // Helper to extract allowed tools per tier from the disclosure manifest.
650
+ function getTierTools(tierData) {
651
+ if (!tierData || !Array.isArray(tierData.allowed_tools)) return [];
652
+ const seen = new Set();
653
+ const out = [];
654
+ for (const tool of tierData.allowed_tools) {
655
+ const cleaned = String(tool || '').trim();
656
+ if (!cleaned) continue;
657
+ const key = cleaned.toLowerCase();
658
+ if (seen.has(key)) continue;
659
+ seen.add(key);
660
+ out.push(cleaned);
661
+ }
662
+ return out;
663
+ }
664
+
649
665
  const tiersData = manifest.tiers || {};
650
666
 
651
667
  // Derive goals from disclosure objectives (used in tier config and token creation)
@@ -659,19 +675,32 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
659
675
  : ['grow-network', 'find-collaborators', 'build-in-public'];
660
676
 
661
677
  try {
662
- config.setTier('public', {
678
+ const publicTools = getTierTools(tiersData.public);
679
+ const friendsTools = [...publicTools, ...getTierTools(tiersData.friends)];
680
+ const familyTools = [...friendsTools, ...getTierTools(tiersData.family)];
681
+
682
+ const publicTierPatch = {
663
683
  topics: getTierTopics(tiersData.public),
664
684
  goals: tokenGoals,
665
685
  disclosure: 'minimal'
666
- });
667
- config.setTier('friends', {
686
+ };
687
+ if (publicTools.length > 0) publicTierPatch.allowed_tools = publicTools;
688
+
689
+ const friendsTierPatch = {
668
690
  topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends)],
669
691
  disclosure: 'standard'
670
- });
671
- config.setTier('family', {
692
+ };
693
+ if (friendsTools.length > 0) friendsTierPatch.allowed_tools = friendsTools;
694
+
695
+ const familyTierPatch = {
672
696
  topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends), ...getTierTopics(tiersData.family)],
673
697
  disclosure: 'full'
674
- });
698
+ };
699
+ if (familyTools.length > 0) familyTierPatch.allowed_tools = familyTools;
700
+
701
+ config.setTier('public', publicTierPatch);
702
+ config.setTier('friends', friendsTierPatch);
703
+ config.setTier('family', familyTierPatch);
675
704
  } catch (err) {
676
705
  console.error(` Warning: could not sync tier config: ${err.message}`);
677
706
  }
@@ -702,6 +731,7 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
702
731
  const hostname = config.getAgent().hostname || process.env.A2A_HOSTNAME || 'localhost';
703
732
 
704
733
  const publicTopics = getTierTopics(tiersData.public);
734
+ const publicTools = getTierTools(tiersData.public);
705
735
 
706
736
  const { token } = store.create({
707
737
  name: agentName,
@@ -712,6 +742,7 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
712
742
  maxCalls: null,
713
743
  allowedTopics: publicTopics,
714
744
  allowedGoals: tokenGoals,
745
+ allowedTools: publicTools.length > 0 ? publicTools : null,
715
746
  notify: 'all'
716
747
  });
717
748
 
@@ -725,6 +756,33 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
725
756
  console.log(` Disclosure: ${MANIFEST_FILE}`);
726
757
  console.log(` Invite: ${inviteUrl}\n`);
727
758
 
759
+ // Native app install should be part of the onboarding tail on macOS so users
760
+ // don't end up launching the app before a2a setup is complete.
761
+ if (os.platform() === 'darwin' && !findNativeApp()) {
762
+ if (isInteractiveShell()) {
763
+ const installNow = await promptYesNo('Install the native macOS app? [Y/n] ');
764
+ if (installNow) {
765
+ const result = installNativeMacApp({ force: false, quiet: false });
766
+ if (result.success) {
767
+ if (result.reason === 'already_current') {
768
+ console.log(`Native app already installed at current version (${result.version}).`);
769
+ console.log(`Path: ${result.appPath}\n`);
770
+ } else {
771
+ console.log(`Native app installed (v${result.version}).`);
772
+ console.log(`Path: ${result.appPath}\n`);
773
+ }
774
+ } else {
775
+ console.warn(`Native app install failed: ${result.error || 'unknown error'}`);
776
+ console.warn('You can retry with: a2a app install\n');
777
+ }
778
+ } else {
779
+ console.log('You can install the native app later with: a2a app install\n');
780
+ }
781
+ } else {
782
+ console.log('Install the native macOS app with: a2a app install\n');
783
+ }
784
+ }
785
+
728
786
  return true;
729
787
  }
730
788
 
@@ -787,6 +845,7 @@ const commands = {
787
845
 
788
846
  // Get tier from --tier or --permissions flag
789
847
  const tier = args.flags.tier || args.flags.t || args.flags.permissions || args.flags.p || 'public';
848
+ const configTier = config.getTiers?.()[tier] || {};
790
849
 
791
850
  // Get owner from flag or config
792
851
  const configAgent = config.getAgent() || {};
@@ -807,6 +866,9 @@ const commands = {
807
866
 
808
867
  // Get objectives from disclosure
809
868
  const objectives = tierTopics.objectives || [];
869
+ const allowedTools = args.flags.tools
870
+ ? String(args.flags.tools).split(',').map(t => t.trim()).filter(Boolean)
871
+ : (Array.isArray(configTier.allowed_tools) ? configTier.allowed_tools : null);
810
872
  const timeoutMsRaw = args.flags['timeout-ms'] || args.flags.timeout_ms;
811
873
  const timeoutMs = timeoutMsRaw ? Number.parseInt(String(timeoutMsRaw), 10) : null;
812
874
 
@@ -820,6 +882,7 @@ const commands = {
820
882
  maxCalls,
821
883
  allowedTopics,
822
884
  allowedGoals: objectives.map(o => o.objective || o),
885
+ allowedTools,
823
886
  timeoutMs
824
887
  });
825
888
 
@@ -856,6 +919,9 @@ const commands = {
856
919
  console.log(`Expires: ${record.expires_at || 'never'}`);
857
920
  console.log(`Tier: ${record.tier}`);
858
921
  console.log(`Topics: ${record.allowed_topics.join(', ')}`);
922
+ if (Array.isArray(record.allowed_tools) && record.allowed_tools.length > 0) {
923
+ console.log(`Tools: ${record.allowed_tools.join(', ')}`);
924
+ }
859
925
  console.log(`Disclosure: ${record.disclosure}`);
860
926
  console.log(`Notify: ${record.notify}`);
861
927
  console.log(`Max calls: ${record.max_calls || 'unlimited'}`);
@@ -2117,6 +2183,9 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
2117
2183
  });
2118
2184
  } else {
2119
2185
  console.log(' Using existing server.');
2186
+ // Persist detected server port even when reusing an already-running process.
2187
+ // Native app discovery depends on onboarding.server_port as a primary hint.
2188
+ config.setOnboarding({ server_port: serverPort });
2120
2189
  }
2121
2190
  console.log(' ✅ A2A server is running');
2122
2191
 
@@ -2282,6 +2351,11 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
2282
2351
  }
2283
2352
 
2284
2353
  if (action === 'install') {
2354
+ if (os.platform() === 'darwin' && !force && !isOnboarded()) {
2355
+ console.error('Onboarding not complete. Run `a2a quickstart` first, then install the app.');
2356
+ console.error('Use `a2a app install --force` to bypass this check.');
2357
+ process.exit(1);
2358
+ }
2285
2359
  const result = installNativeMacApp({ force, quiet });
2286
2360
  if (result.skipped === 'not_macos') {
2287
2361
  console.error('Native app install is only available on macOS.');
@@ -2856,6 +2930,7 @@ Commands:
2856
2930
  --expires, -e Expiration (1h, 1d, 7d, 30d, never)
2857
2931
  --permissions, -p Tier (public, friends, family)
2858
2932
  --topics Custom topics (comma-separated, overrides tier defaults)
2933
+ --tools Custom tool allowlist (comma-separated, overrides tier defaults)
2859
2934
  --disclosure, -d Disclosure level (public, minimal, none)
2860
2935
  --notify Owner notification (all, summary, none)
2861
2936
  --max-calls Maximum invocations (default: 100)
@@ -2904,7 +2979,7 @@ Calling:
2904
2979
  app Manage native macOS app
2905
2980
  status Show native app installation status (default)
2906
2981
  install Install/update native app from GitHub release
2907
- --force, -f Reinstall even when current version is present
2982
+ --force, -f Reinstall/bypass onboarding guard
2908
2983
  --quiet, -q Suppress download/extract output
2909
2984
  uninstall Remove native app from ~/Applications
2910
2985
 
@@ -2939,6 +3014,7 @@ Server:
2939
3014
  Examples:
2940
3015
  a2a create --name "bappybot" --owner "Benjamin Pollack" --expires 7d
2941
3016
  a2a create --name "custom" --topics "chat,calendar.read,email.read"
3017
+ a2a create --name "research" --tools "Read,Grep,Glob,WebSearch,WebFetch"
2942
3018
  a2a contacts add a2a://host/fed_xxx --name "Alice" --owner "Alice Chen"
2943
3019
  a2a contacts link Alice tok_abc123
2944
3020
  a2a call Alice "Hello!"
package/docs/protocol.md CHANGED
@@ -155,11 +155,11 @@ Example filters for `/logs`: `trace_id`, `conversation_id`, `token_id`, `error_c
155
155
 
156
156
  ## Permission Tiers
157
157
 
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` |
158
+ | Tier | Default capabilities | Default `allowed_tools` |
159
+ |------|----------------------|--------------------------|
160
+ | `public` | `context-read` | `Read`, `Grep`, `Glob` |
161
+ | `friends` | `context-read`, `calendar.read`, `email.read`, `search` | `Bash(readonly)`, `Read`, `Grep`, `Glob`, `WebSearch`, `WebFetch` |
162
+ | `family` | `context-read`, `calendar`, `email`, `search`, `tools`, `memory` | `Bash`, `Read`, `Grep`, `Glob`, `WebSearch`, `WebFetch` |
163
163
 
164
164
  ## Disclosure Levels
165
165
 
@@ -184,6 +184,7 @@ Stored in `~/.config/openclaw/a2a.json`:
184
184
  "capabilities": ["context-read"],
185
185
  "allowed_topics": ["chat"],
186
186
  "allowed_goals": [],
187
+ "allowed_tools": ["Read", "Grep", "Glob"],
187
188
  "tier_settings": {},
188
189
  "disclosure": "minimal",
189
190
  "notify": "all",
@@ -62,7 +62,7 @@
62
62
  <span class="status-indicator searching"></span>
63
63
  Looking for a2a server...
64
64
  </p>
65
- <p class="port-info" id="port-info">Scanning ports: 3001, 80, 8080, 8443, 9001</p>
65
+ <p class="port-info" id="port-info">Scanning ports: 80, 3001, 8080, 8443, 9001</p>
66
66
  </div>
67
67
 
68
68
  <div id="status-not-found" style="display:none;">
@@ -86,10 +86,22 @@
86
86
  </div>
87
87
 
88
88
  <script>
89
- const { invoke } = window.__TAURI__.core;
89
+ const tauriCore = window.__TAURI__ && window.__TAURI__.core;
90
+ const tauriEvents = window.__TAURI__ && window.__TAURI__.event;
91
+ const legacyInvoke = window.__TAURI_INTERNALS__ && window.__TAURI_INTERNALS__.invoke;
92
+ const invoke = (tauriCore && tauriCore.invoke)
93
+ ? tauriCore.invoke
94
+ : (legacyInvoke ? ((cmd, args) => legacyInvoke(cmd, args)) : null);
90
95
 
91
96
  async function checkServer() {
92
97
  show('status-searching');
98
+ if (!invoke) {
99
+ show('status-not-found');
100
+ const detail = document.getElementById('error-detail');
101
+ detail.textContent = 'Tauri bridge unavailable. Reinstall with `a2a app install --force`.';
102
+ detail.style.display = 'block';
103
+ return;
104
+ }
93
105
  try {
94
106
  const result = await invoke('discover_server');
95
107
  if (result.port) {
@@ -132,16 +144,16 @@
132
144
  checkServer();
133
145
 
134
146
  // Listen for server disconnect/reconnect from Tauri backend
135
- const { listen } = window.__TAURI__.event;
136
-
137
- listen('server-status', (event) => {
138
- const { connected, port } = event.payload;
139
- if (!connected) {
140
- showReconnectionOverlay();
141
- } else {
142
- hideReconnectionOverlay();
143
- }
144
- });
147
+ if (tauriEvents && tauriEvents.listen) {
148
+ tauriEvents.listen('server-status', (event) => {
149
+ const { connected } = event.payload;
150
+ if (!connected) {
151
+ showReconnectionOverlay();
152
+ } else {
153
+ hideReconnectionOverlay();
154
+ }
155
+ });
156
+ }
145
157
 
146
158
  function showReconnectionOverlay() {
147
159
  if (document.getElementById('reconnect-overlay')) return;
@@ -0,0 +1,232 @@
1
+ {
2
+ "name": "a2a-callbook-macos",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "a2a-callbook-macos",
9
+ "version": "0.1.0",
10
+ "devDependencies": {
11
+ "@tauri-apps/cli": "^2.10.0"
12
+ }
13
+ },
14
+ "node_modules/@tauri-apps/cli": {
15
+ "version": "2.10.0",
16
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.0.tgz",
17
+ "integrity": "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==",
18
+ "dev": true,
19
+ "license": "Apache-2.0 OR MIT",
20
+ "bin": {
21
+ "tauri": "tauri.js"
22
+ },
23
+ "engines": {
24
+ "node": ">= 10"
25
+ },
26
+ "funding": {
27
+ "type": "opencollective",
28
+ "url": "https://opencollective.com/tauri"
29
+ },
30
+ "optionalDependencies": {
31
+ "@tauri-apps/cli-darwin-arm64": "2.10.0",
32
+ "@tauri-apps/cli-darwin-x64": "2.10.0",
33
+ "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0",
34
+ "@tauri-apps/cli-linux-arm64-gnu": "2.10.0",
35
+ "@tauri-apps/cli-linux-arm64-musl": "2.10.0",
36
+ "@tauri-apps/cli-linux-riscv64-gnu": "2.10.0",
37
+ "@tauri-apps/cli-linux-x64-gnu": "2.10.0",
38
+ "@tauri-apps/cli-linux-x64-musl": "2.10.0",
39
+ "@tauri-apps/cli-win32-arm64-msvc": "2.10.0",
40
+ "@tauri-apps/cli-win32-ia32-msvc": "2.10.0",
41
+ "@tauri-apps/cli-win32-x64-msvc": "2.10.0"
42
+ }
43
+ },
44
+ "node_modules/@tauri-apps/cli-darwin-arm64": {
45
+ "version": "2.10.0",
46
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.0.tgz",
47
+ "integrity": "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==",
48
+ "cpu": [
49
+ "arm64"
50
+ ],
51
+ "dev": true,
52
+ "license": "Apache-2.0 OR MIT",
53
+ "optional": true,
54
+ "os": [
55
+ "darwin"
56
+ ],
57
+ "engines": {
58
+ "node": ">= 10"
59
+ }
60
+ },
61
+ "node_modules/@tauri-apps/cli-darwin-x64": {
62
+ "version": "2.10.0",
63
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.0.tgz",
64
+ "integrity": "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==",
65
+ "cpu": [
66
+ "x64"
67
+ ],
68
+ "dev": true,
69
+ "license": "Apache-2.0 OR MIT",
70
+ "optional": true,
71
+ "os": [
72
+ "darwin"
73
+ ],
74
+ "engines": {
75
+ "node": ">= 10"
76
+ }
77
+ },
78
+ "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
79
+ "version": "2.10.0",
80
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.0.tgz",
81
+ "integrity": "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==",
82
+ "cpu": [
83
+ "arm"
84
+ ],
85
+ "dev": true,
86
+ "license": "Apache-2.0 OR MIT",
87
+ "optional": true,
88
+ "os": [
89
+ "linux"
90
+ ],
91
+ "engines": {
92
+ "node": ">= 10"
93
+ }
94
+ },
95
+ "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
96
+ "version": "2.10.0",
97
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.0.tgz",
98
+ "integrity": "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==",
99
+ "cpu": [
100
+ "arm64"
101
+ ],
102
+ "dev": true,
103
+ "license": "Apache-2.0 OR MIT",
104
+ "optional": true,
105
+ "os": [
106
+ "linux"
107
+ ],
108
+ "engines": {
109
+ "node": ">= 10"
110
+ }
111
+ },
112
+ "node_modules/@tauri-apps/cli-linux-arm64-musl": {
113
+ "version": "2.10.0",
114
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.0.tgz",
115
+ "integrity": "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==",
116
+ "cpu": [
117
+ "arm64"
118
+ ],
119
+ "dev": true,
120
+ "license": "Apache-2.0 OR MIT",
121
+ "optional": true,
122
+ "os": [
123
+ "linux"
124
+ ],
125
+ "engines": {
126
+ "node": ">= 10"
127
+ }
128
+ },
129
+ "node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
130
+ "version": "2.10.0",
131
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.0.tgz",
132
+ "integrity": "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==",
133
+ "cpu": [
134
+ "riscv64"
135
+ ],
136
+ "dev": true,
137
+ "license": "Apache-2.0 OR MIT",
138
+ "optional": true,
139
+ "os": [
140
+ "linux"
141
+ ],
142
+ "engines": {
143
+ "node": ">= 10"
144
+ }
145
+ },
146
+ "node_modules/@tauri-apps/cli-linux-x64-gnu": {
147
+ "version": "2.10.0",
148
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.0.tgz",
149
+ "integrity": "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==",
150
+ "cpu": [
151
+ "x64"
152
+ ],
153
+ "dev": true,
154
+ "license": "Apache-2.0 OR MIT",
155
+ "optional": true,
156
+ "os": [
157
+ "linux"
158
+ ],
159
+ "engines": {
160
+ "node": ">= 10"
161
+ }
162
+ },
163
+ "node_modules/@tauri-apps/cli-linux-x64-musl": {
164
+ "version": "2.10.0",
165
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.0.tgz",
166
+ "integrity": "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==",
167
+ "cpu": [
168
+ "x64"
169
+ ],
170
+ "dev": true,
171
+ "license": "Apache-2.0 OR MIT",
172
+ "optional": true,
173
+ "os": [
174
+ "linux"
175
+ ],
176
+ "engines": {
177
+ "node": ">= 10"
178
+ }
179
+ },
180
+ "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
181
+ "version": "2.10.0",
182
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.0.tgz",
183
+ "integrity": "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==",
184
+ "cpu": [
185
+ "arm64"
186
+ ],
187
+ "dev": true,
188
+ "license": "Apache-2.0 OR MIT",
189
+ "optional": true,
190
+ "os": [
191
+ "win32"
192
+ ],
193
+ "engines": {
194
+ "node": ">= 10"
195
+ }
196
+ },
197
+ "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
198
+ "version": "2.10.0",
199
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.0.tgz",
200
+ "integrity": "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==",
201
+ "cpu": [
202
+ "ia32"
203
+ ],
204
+ "dev": true,
205
+ "license": "Apache-2.0 OR MIT",
206
+ "optional": true,
207
+ "os": [
208
+ "win32"
209
+ ],
210
+ "engines": {
211
+ "node": ">= 10"
212
+ }
213
+ },
214
+ "node_modules/@tauri-apps/cli-win32-x64-msvc": {
215
+ "version": "2.10.0",
216
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.0.tgz",
217
+ "integrity": "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==",
218
+ "cpu": [
219
+ "x64"
220
+ ],
221
+ "dev": true,
222
+ "license": "Apache-2.0 OR MIT",
223
+ "optional": true,
224
+ "os": [
225
+ "win32"
226
+ ],
227
+ "engines": {
228
+ "node": ">= 10"
229
+ }
230
+ }
231
+ }
232
+ }
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
2
2
  use std::path::PathBuf;
3
3
  use std::time::Duration;
4
4
 
5
- const DEFAULT_PORTS: &[u16] = &[3001, 80, 8080, 8443, 9001];
5
+ const DEFAULT_PORTS: &[u16] = &[80, 3001, 8080, 8443, 9001];
6
6
  const PROBE_TIMEOUT: Duration = Duration::from_millis(800);
7
7
 
8
8
  #[derive(Debug, Serialize, Deserialize)]
@@ -14,15 +14,57 @@ pub struct DiscoveryResult {
14
14
  #[derive(Debug, Deserialize)]
15
15
  struct A2AConfig {
16
16
  onboarding: Option<OnboardingConfig>,
17
+ agent: Option<AgentConfig>,
17
18
  }
18
19
 
19
20
  #[derive(Debug, Deserialize)]
20
21
  struct OnboardingConfig {
22
+ #[serde(alias = "serverPort")]
21
23
  server_port: Option<u16>,
22
24
  }
23
25
 
24
- /// Read port from ~/.config/openclaw/a2a-config.json
25
- pub fn read_config_port() -> Option<u16> {
26
+ #[derive(Debug, Deserialize)]
27
+ struct AgentConfig {
28
+ hostname: Option<String>,
29
+ }
30
+
31
+ fn parse_port_from_hostname(hostname: &str) -> Option<u16> {
32
+ let trimmed = hostname.trim();
33
+ if trimmed.is_empty() {
34
+ return None;
35
+ }
36
+
37
+ // Allow values like:
38
+ // - 149.28.213.47:3007
39
+ // - localhost:3007
40
+ // - http://localhost:3007
41
+ // - [::1]:3007
42
+ let without_scheme = trimmed.split("://").nth(1).unwrap_or(trimmed);
43
+ let host_segment = without_scheme.split('/').next().unwrap_or(without_scheme);
44
+
45
+ if host_segment.starts_with('[') {
46
+ let end = host_segment.find(']')?;
47
+ let remainder = &host_segment[end + 1..];
48
+ let port_str = remainder.strip_prefix(':')?;
49
+ return port_str.parse::<u16>().ok();
50
+ }
51
+
52
+ // Bare IPv6 literals contain multiple colons and no explicit port delimiter.
53
+ if host_segment.matches(':').count() > 1 {
54
+ return None;
55
+ }
56
+
57
+ let (_, port_str) = host_segment.rsplit_once(':')?;
58
+ if !port_str.chars().all(|ch| ch.is_ascii_digit()) {
59
+ return None;
60
+ }
61
+
62
+ port_str.parse::<u16>().ok()
63
+ }
64
+
65
+ /// Read likely server ports from ~/.config/openclaw/a2a-config.json.
66
+ /// We prefer explicit onboarding server_port, then agent.hostname port.
67
+ pub fn read_config_ports() -> Vec<u16> {
26
68
  let config_dir = std::env::var("A2A_CONFIG_DIR")
27
69
  .or_else(|_| std::env::var("OPENCLAW_CONFIG_DIR"))
28
70
  .map(PathBuf::from)
@@ -34,14 +76,36 @@ pub fn read_config_port() -> Option<u16> {
34
76
  });
35
77
 
36
78
  let config_path = config_dir.join("a2a-config.json");
37
- let content = std::fs::read_to_string(config_path).ok()?;
38
- let config: A2AConfig = serde_json::from_str(&content).ok()?;
39
- config.onboarding?.server_port
79
+ let content = match std::fs::read_to_string(config_path) {
80
+ Ok(data) => data,
81
+ Err(_) => return vec![],
82
+ };
83
+ let config: A2AConfig = match serde_json::from_str(&content) {
84
+ Ok(parsed) => parsed,
85
+ Err(_) => return vec![],
86
+ };
87
+
88
+ let mut ports = Vec::new();
89
+
90
+ if let Some(port) = config.onboarding.and_then(|ob| ob.server_port) {
91
+ ports.push(port);
92
+ }
93
+
94
+ if let Some(port) = config
95
+ .agent
96
+ .and_then(|agent| agent.hostname)
97
+ .and_then(|hostname| parse_port_from_hostname(&hostname))
98
+ {
99
+ if !ports.contains(&port) {
100
+ ports.push(port);
101
+ }
102
+ }
103
+
104
+ ports
40
105
  }
41
106
 
42
107
  /// Probe a single port — returns true if a2a server responds
43
108
  async fn probe_port(port: u16) -> bool {
44
- let url = format!("http://127.0.0.1:{}/api/a2a/ping", port);
45
109
  let client = reqwest::Client::builder()
46
110
  .timeout(PROBE_TIMEOUT)
47
111
  .build();
@@ -51,16 +115,36 @@ async fn probe_port(port: u16) -> bool {
51
115
  Err(_) => return false,
52
116
  };
53
117
 
54
- match client.get(&url).send().await {
55
- Ok(resp) => resp.status().is_success(),
56
- Err(_) => false,
118
+ for host in ["127.0.0.1", "localhost"] {
119
+ let url = format!("http://{}:{}/api/a2a/ping", host, port);
120
+ let ok = match client.get(&url).send().await {
121
+ Ok(resp) => {
122
+ if !resp.status().is_success() {
123
+ false
124
+ } else {
125
+ match resp.text().await {
126
+ Ok(body) => body.contains("\"pong\":true"),
127
+ Err(_) => false,
128
+ }
129
+ }
130
+ }
131
+ Err(_) => false,
132
+ };
133
+
134
+ if ok {
135
+ return true;
136
+ }
57
137
  }
138
+
139
+ false
58
140
  }
59
141
 
60
142
  /// Discover the running a2a server
61
143
  pub async fn discover_server() -> DiscoveryResult {
62
- // 1. Try config port first
63
- if let Some(port) = read_config_port() {
144
+ let config_ports = read_config_ports();
145
+
146
+ // 1. Try config-derived ports first
147
+ for &port in &config_ports {
64
148
  if probe_port(port).await {
65
149
  return DiscoveryResult {
66
150
  port: Some(port),
@@ -69,8 +153,11 @@ pub async fn discover_server() -> DiscoveryResult {
69
153
  }
70
154
  }
71
155
 
72
- // 2. Scan default ports
156
+ // 2. Scan default ports (skip those already checked from config)
73
157
  for &port in DEFAULT_PORTS {
158
+ if config_ports.contains(&port) {
159
+ continue;
160
+ }
74
161
  if probe_port(port).await {
75
162
  return DiscoveryResult {
76
163
  port: Some(port),