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/README.md +33 -9
- package/SKILL.md +67 -5
- package/bin/cli.js +468 -151
- package/docs/protocol.md +24 -14
- package/package.json +1 -1
- package/scripts/install-openclaw.js +64 -68
- package/src/dashboard/public/app.js +765 -28
- package/src/dashboard/public/index.html +57 -13
- package/src/dashboard/public/style.css +16 -0
- package/src/lib/callbook.js +358 -0
- package/src/lib/client.js +1 -2
- package/src/lib/config.js +67 -15
- package/src/lib/external-ip.js +18 -7
- package/src/lib/invite-host.js +26 -41
- package/src/lib/logger.js +26 -14
- package/src/lib/tokens.js +314 -113
- package/src/routes/a2a.js +11 -2
- package/src/routes/callbook.js +142 -0
- package/src/routes/dashboard.js +557 -25
- package/src/server.js +6 -0
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
|
|
156
|
+
## Permission Tiers
|
|
157
157
|
|
|
158
|
-
|
|
|
159
|
-
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
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": "
|
|
180
|
+
"id": "tok_abc123xyz789",
|
|
181
181
|
"token_hash": "sha256...",
|
|
182
182
|
"name": "Alice's agent",
|
|
183
|
-
"
|
|
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
|
-
"
|
|
198
|
+
"contacts": [
|
|
195
199
|
{
|
|
196
|
-
"id": "
|
|
200
|
+
"id": "contact_xyz",
|
|
197
201
|
"name": "Bob's agent",
|
|
202
|
+
"owner": "Bob",
|
|
198
203
|
"host": "bob.example.com",
|
|
199
|
-
"
|
|
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
|
-
"
|
|
291
|
+
"tier": "public",
|
|
282
292
|
"disclosure": "minimal"
|
|
283
293
|
}
|
|
284
294
|
}
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
916
|
-
/a2a call <url> <msg> Call a
|
|
911
|
+
/a2a add <url> Add a contact
|
|
912
|
+
/a2a call <url> <msg> Call a contact
|
|
917
913
|
|
|
918
914
|
${bold('━━━ Done! ━━━')}
|
|
919
915
|
|