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/src/lib/external-ip.js
CHANGED
|
@@ -22,6 +22,7 @@ const CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
|
|
|
22
22
|
const EXTERNAL_IP_CACHE_FILE = path.join(CONFIG_DIR, 'a2a-external-ip.json');
|
|
23
23
|
|
|
24
24
|
const DEFAULT_SERVICES = [
|
|
25
|
+
'https://ifconfig.me/ip',
|
|
25
26
|
'https://api.ipify.org',
|
|
26
27
|
'https://checkip.amazonaws.com/',
|
|
27
28
|
'https://icanhazip.com/'
|
|
@@ -113,6 +114,7 @@ async function fetchExternalIp(options = {}) {
|
|
|
113
114
|
? options.services
|
|
114
115
|
: DEFAULT_SERVICES;
|
|
115
116
|
|
|
117
|
+
const attempts = [];
|
|
116
118
|
let lastError = null;
|
|
117
119
|
for (const serviceUrl of services) {
|
|
118
120
|
try {
|
|
@@ -124,14 +126,18 @@ async function fetchExternalIp(options = {}) {
|
|
|
124
126
|
if (!ip) {
|
|
125
127
|
throw new Error('invalid_ip');
|
|
126
128
|
}
|
|
127
|
-
|
|
129
|
+
attempts.push({ service: serviceUrl, ok: true, statusCode: res.statusCode, ip });
|
|
130
|
+
return { ip, source: serviceUrl, attempts };
|
|
128
131
|
} catch (err) {
|
|
129
132
|
lastError = err;
|
|
133
|
+
attempts.push({ service: serviceUrl, ok: false, error: err && err.message ? err.message : 'request_failed' });
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
const msg = lastError ? lastError.message : 'unavailable';
|
|
134
|
-
|
|
138
|
+
const failure = new Error(`external_ip_unavailable:${msg}`);
|
|
139
|
+
failure.attempts = attempts;
|
|
140
|
+
throw failure;
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
/**
|
|
@@ -168,13 +174,13 @@ async function getExternalIp(options = {}) {
|
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
try {
|
|
171
|
-
const { ip, source } = await fetchExternalIp({
|
|
177
|
+
const { ip, source, attempts } = await fetchExternalIp({
|
|
172
178
|
timeoutMs: options.timeoutMs,
|
|
173
179
|
services: options.services
|
|
174
180
|
});
|
|
175
181
|
const checkedAt = new Date(nowMs).toISOString();
|
|
176
182
|
atomicWriteJson(cacheFile, { ip, checked_at: checkedAt, source });
|
|
177
|
-
return { ip, checkedAt, source, fromCache: false, stale: false };
|
|
183
|
+
return { ip, checkedAt, source, fromCache: false, stale: false, attempts: Array.isArray(attempts) ? attempts : null };
|
|
178
184
|
} catch (err) {
|
|
179
185
|
if (cached && cached.ip) {
|
|
180
186
|
const cachedIp = parseIp(cached.ip);
|
|
@@ -184,11 +190,17 @@ async function getExternalIp(options = {}) {
|
|
|
184
190
|
checkedAt: cached.checked_at || null,
|
|
185
191
|
source: cached.source || 'cache',
|
|
186
192
|
fromCache: true,
|
|
187
|
-
stale: true
|
|
193
|
+
stale: true,
|
|
194
|
+
error: err && err.message ? err.message : 'external_ip_unavailable',
|
|
195
|
+
attempts: err && Array.isArray(err.attempts) ? err.attempts : null
|
|
188
196
|
};
|
|
189
197
|
}
|
|
190
198
|
}
|
|
191
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
ip: null,
|
|
201
|
+
error: err && err.message ? err.message : 'external_ip_unavailable',
|
|
202
|
+
attempts: err && Array.isArray(err.attempts) ? err.attempts : null
|
|
203
|
+
};
|
|
192
204
|
}
|
|
193
205
|
}
|
|
194
206
|
|
|
@@ -197,4 +209,3 @@ module.exports = {
|
|
|
197
209
|
fetchExternalIp,
|
|
198
210
|
getExternalIp
|
|
199
211
|
};
|
|
200
|
-
|
package/src/lib/invite-host.js
CHANGED
|
@@ -124,14 +124,6 @@ function isPublicIpHostname(hostname) {
|
|
|
124
124
|
return false;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
function isLegacyTunnelHostname(hostname) {
|
|
128
|
-
const host = String(hostname || '').trim().toLowerCase();
|
|
129
|
-
if (!host) return false;
|
|
130
|
-
// Legacy: Cloudflare Quick Tunnel hostnames were ephemeral and are now unsupported.
|
|
131
|
-
if (host.endsWith('.trycloudflare.com')) return true;
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
127
|
async function resolveInviteHost(options = {}) {
|
|
136
128
|
const config = options.config || null;
|
|
137
129
|
|
|
@@ -155,8 +147,7 @@ async function resolveInviteHost(options = {}) {
|
|
|
155
147
|
: 'default';
|
|
156
148
|
|
|
157
149
|
const parsed = splitHostPort(candidate);
|
|
158
|
-
const
|
|
159
|
-
const desiredPort = (candidateIsLegacyTunnel ? null : parsed.port) ||
|
|
150
|
+
const desiredPort = parsed.port ||
|
|
160
151
|
Number.parseInt(String(options.defaultPort || ''), 10) ||
|
|
161
152
|
readIntEnv('PORT') ||
|
|
162
153
|
readIntEnv('A2A_PORT') ||
|
|
@@ -170,38 +161,28 @@ async function resolveInviteHost(options = {}) {
|
|
|
170
161
|
? options.externalIpTtlMs
|
|
171
162
|
: undefined;
|
|
172
163
|
|
|
173
|
-
// If a previous run persisted a legacy Cloudflare Quick Tunnel hostname into config (e.g. trycloudflare),
|
|
174
|
-
// treat it like "unroutable" so we don't emit stale/unsupported invite endpoints.
|
|
175
|
-
if (candidateIsLegacyTunnel) {
|
|
176
|
-
warnings.push(
|
|
177
|
-
`Detected legacy Quick Tunnel hostname "${candidateHostWithPort}". Quick Tunnel support was removed. ` +
|
|
178
|
-
`Set A2A_HOSTNAME="your-public-host:port" (or wire a reverse proxy) to enable internet-facing invites.`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
164
|
const shouldReplaceWithExternalIp = isLocalOrUnroutableHost(parsed.hostname) ||
|
|
183
|
-
|
|
165
|
+
(
|
|
184
166
|
options.refreshExternalIp && isPublicIpHostname(parsed.hostname)
|
|
185
167
|
);
|
|
186
168
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (external && external.ip) {
|
|
169
|
+
const alwaysLookupExternalIp = Boolean(options.alwaysLookupExternalIp);
|
|
170
|
+
const wantsExternalIp = shouldReplaceWithExternalIp || alwaysLookupExternalIp;
|
|
171
|
+
const warnOnExternalIpFailure = options.warnOnExternalIpFailure !== undefined
|
|
172
|
+
? Boolean(options.warnOnExternalIpFailure)
|
|
173
|
+
: shouldReplaceWithExternalIp;
|
|
174
|
+
|
|
175
|
+
const external = wantsExternalIp
|
|
176
|
+
? await getExternalIp({
|
|
177
|
+
ttlMs,
|
|
178
|
+
timeoutMs: options.externalIpTimeoutMs,
|
|
179
|
+
services: options.externalIpServices,
|
|
180
|
+
cacheFile: options.externalIpCacheFile,
|
|
181
|
+
forceRefresh: Boolean(options.forceRefreshExternalIp)
|
|
182
|
+
})
|
|
183
|
+
: null;
|
|
184
|
+
|
|
185
|
+
if (shouldReplaceWithExternalIp && external && external.ip) {
|
|
205
186
|
const finalHost = formatHostPort(external.ip, desiredPort);
|
|
206
187
|
if (finalHost !== candidateHostWithPort) {
|
|
207
188
|
warnings.push(
|
|
@@ -213,17 +194,21 @@ async function resolveInviteHost(options = {}) {
|
|
|
213
194
|
source: 'external_ip',
|
|
214
195
|
originalHost: candidateHostWithPort,
|
|
215
196
|
externalIp: external.ip,
|
|
197
|
+
externalIpInfo: external,
|
|
216
198
|
warnings
|
|
217
199
|
};
|
|
218
200
|
}
|
|
219
201
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
202
|
+
if (wantsExternalIp && (!external || !external.ip) && warnOnExternalIpFailure) {
|
|
203
|
+
warnings.push(
|
|
204
|
+
`Invite host "${candidateHostWithPort}" may not be reachable from other machines, and external IP lookup failed. Set A2A_HOSTNAME="your-public-host:port".`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
223
207
|
return {
|
|
224
208
|
host: candidateHostWithPort,
|
|
225
209
|
source: candidateSource,
|
|
226
210
|
originalHost: candidateHostWithPort,
|
|
211
|
+
externalIpInfo: external,
|
|
227
212
|
warnings
|
|
228
213
|
};
|
|
229
214
|
}
|
package/src/lib/logger.js
CHANGED
|
@@ -129,7 +129,28 @@ class LogStore {
|
|
|
129
129
|
} catch (err) {
|
|
130
130
|
// best effort
|
|
131
131
|
}
|
|
132
|
-
this.
|
|
132
|
+
const ok = this._ensureSchema();
|
|
133
|
+
if (!ok) {
|
|
134
|
+
// Prototyping mode: do not attempt DB migrations; keep the old file and start fresh.
|
|
135
|
+
const backupPath = `${this.dbPath}.legacy.${Date.now()}`;
|
|
136
|
+
try {
|
|
137
|
+
this.db.close();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
// ignore
|
|
140
|
+
}
|
|
141
|
+
fs.renameSync(this.dbPath, backupPath);
|
|
142
|
+
this.db = new Database(this.dbPath);
|
|
143
|
+
try {
|
|
144
|
+
fs.chmodSync(this.dbPath, 0o600);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
// best effort
|
|
147
|
+
}
|
|
148
|
+
const ok2 = this._ensureSchema();
|
|
149
|
+
if (!ok2) {
|
|
150
|
+
this._dbError = 'failed_to_initialize_log_db_schema';
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
133
154
|
this._prepareStatements();
|
|
134
155
|
return this.db;
|
|
135
156
|
} catch (err) {
|
|
@@ -138,7 +159,7 @@ class LogStore {
|
|
|
138
159
|
}
|
|
139
160
|
}
|
|
140
161
|
|
|
141
|
-
|
|
162
|
+
_ensureSchema() {
|
|
142
163
|
this.db.exec(`
|
|
143
164
|
CREATE TABLE IF NOT EXISTS logs (
|
|
144
165
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -167,18 +188,9 @@ class LogStore {
|
|
|
167
188
|
`);
|
|
168
189
|
|
|
169
190
|
const columns = this.db.prepare(`PRAGMA table_info(logs)`).all();
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
if (!hasErrorCode) {
|
|
174
|
-
this.db.exec(`ALTER TABLE logs ADD COLUMN error_code TEXT`);
|
|
175
|
-
}
|
|
176
|
-
if (!hasStatusCode) {
|
|
177
|
-
this.db.exec(`ALTER TABLE logs ADD COLUMN status_code INTEGER`);
|
|
178
|
-
}
|
|
179
|
-
if (!hasHint) {
|
|
180
|
-
this.db.exec(`ALTER TABLE logs ADD COLUMN hint TEXT`);
|
|
181
|
-
}
|
|
191
|
+
const names = new Set(columns.map(c => c && c.name).filter(Boolean));
|
|
192
|
+
const required = ['timestamp', 'level', 'component', 'message', 'error_code', 'status_code', 'hint', 'data'];
|
|
193
|
+
return required.every((name) => names.has(name));
|
|
182
194
|
}
|
|
183
195
|
|
|
184
196
|
_prepareStatements() {
|