a2acalling 0.6.1 → 0.6.3
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 +69 -5
- package/bin/cli.js +539 -162
- package/docs/protocol.md +24 -14
- package/package.json +1 -1
- package/scripts/install-openclaw.js +64 -64
- 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 +214 -16
- package/src/lib/conversations.js +74 -0
- package/src/lib/disclosure.js +3 -43
- package/src/lib/external-ip.js +18 -7
- package/src/lib/invite-host.js +24 -21
- 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 +605 -37
- package/src/server.js +6 -0
package/src/lib/config.js
CHANGED
|
@@ -15,9 +15,165 @@ const CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
|
|
|
15
15
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'a2a-config.json');
|
|
16
16
|
const logger = createLogger({ component: 'a2a.config' });
|
|
17
17
|
|
|
18
|
+
function isPlainObject(value) {
|
|
19
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function configValidationError(code, message, hint, data) {
|
|
23
|
+
const err = new Error(message);
|
|
24
|
+
err.code = code;
|
|
25
|
+
if (hint) err.hint = hint;
|
|
26
|
+
if (data) err.data = data;
|
|
27
|
+
return err;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sanitizeString(value, maxLength = 200) {
|
|
31
|
+
return String(value || '')
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim()
|
|
34
|
+
.slice(0, maxLength);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateStringArray(value, label, options = {}) {
|
|
38
|
+
const maxItems = Number.isFinite(options.maxItems) ? options.maxItems : 200;
|
|
39
|
+
const itemMaxLength = Number.isFinite(options.itemMaxLength) ? options.itemMaxLength : 160;
|
|
40
|
+
|
|
41
|
+
if (!Array.isArray(value)) {
|
|
42
|
+
throw configValidationError(
|
|
43
|
+
'A2A_CONFIG_INVALID_ARRAY',
|
|
44
|
+
`Invalid ${label}: expected an array of strings`,
|
|
45
|
+
'Pass an array like ["chat","search"].',
|
|
46
|
+
{ label, received_type: typeof value }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const unique = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
for (const entry of value) {
|
|
53
|
+
if (typeof entry !== 'string') {
|
|
54
|
+
throw configValidationError(
|
|
55
|
+
'A2A_CONFIG_INVALID_ARRAY_ITEM',
|
|
56
|
+
`Invalid ${label}: each item must be a string`,
|
|
57
|
+
'Ensure your tier topics/goals are string arrays.',
|
|
58
|
+
{ label, received_item_type: typeof entry }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const cleaned = sanitizeString(entry, itemMaxLength);
|
|
62
|
+
if (!cleaned) continue;
|
|
63
|
+
const key = cleaned.toLowerCase();
|
|
64
|
+
if (seen.has(key)) continue;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
unique.push(cleaned);
|
|
67
|
+
if (unique.length >= maxItems) break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return unique;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateTierPatch(tierName, tierConfig) {
|
|
74
|
+
if (!isPlainObject(tierConfig)) {
|
|
75
|
+
throw configValidationError(
|
|
76
|
+
'A2A_CONFIG_INVALID_TIER_PATCH',
|
|
77
|
+
`Invalid tier config for "${tierName}": expected an object`,
|
|
78
|
+
'Pass an object like { topics: [...], goals: [...] }.',
|
|
79
|
+
{ tier: tierName, received_type: typeof tierConfig }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const out = {};
|
|
84
|
+
|
|
85
|
+
if (tierConfig.name !== undefined) {
|
|
86
|
+
if (typeof tierConfig.name !== 'string') {
|
|
87
|
+
throw configValidationError(
|
|
88
|
+
'A2A_CONFIG_INVALID_TIER_NAME',
|
|
89
|
+
`Invalid tier name for "${tierName}": expected string`,
|
|
90
|
+
null,
|
|
91
|
+
{ tier: tierName, received_type: typeof tierConfig.name }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
out.name = sanitizeString(tierConfig.name, 120);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (tierConfig.description !== undefined) {
|
|
98
|
+
if (typeof tierConfig.description !== 'string') {
|
|
99
|
+
throw configValidationError(
|
|
100
|
+
'A2A_CONFIG_INVALID_TIER_DESCRIPTION',
|
|
101
|
+
`Invalid tier description for "${tierName}": expected string`,
|
|
102
|
+
null,
|
|
103
|
+
{ tier: tierName, received_type: typeof tierConfig.description }
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
out.description = sanitizeString(tierConfig.description, 300);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (tierConfig.disclosure !== undefined) {
|
|
110
|
+
if (typeof tierConfig.disclosure !== 'string') {
|
|
111
|
+
throw configValidationError(
|
|
112
|
+
'A2A_CONFIG_INVALID_TIER_DISCLOSURE',
|
|
113
|
+
`Invalid tier disclosure for "${tierName}": expected string`,
|
|
114
|
+
null,
|
|
115
|
+
{ tier: tierName, received_type: typeof tierConfig.disclosure }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
out.disclosure = sanitizeString(tierConfig.disclosure, 40) || 'minimal';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (tierConfig.capabilities !== undefined) {
|
|
122
|
+
out.capabilities = validateStringArray(tierConfig.capabilities, `${tierName}.capabilities`, {
|
|
123
|
+
maxItems: 100,
|
|
124
|
+
itemMaxLength: 120
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (tierConfig.topics !== undefined) {
|
|
129
|
+
out.topics = validateStringArray(tierConfig.topics, `${tierName}.topics`, {
|
|
130
|
+
maxItems: 200,
|
|
131
|
+
itemMaxLength: 160
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (tierConfig.goals !== undefined) {
|
|
136
|
+
out.goals = validateStringArray(tierConfig.goals, `${tierName}.goals`, {
|
|
137
|
+
maxItems: 200,
|
|
138
|
+
itemMaxLength: 160
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (tierConfig.examples !== undefined) {
|
|
143
|
+
out.examples = validateStringArray(tierConfig.examples, `${tierName}.examples`, {
|
|
144
|
+
maxItems: 20,
|
|
145
|
+
itemMaxLength: 120
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function deepMerge(base, override) {
|
|
153
|
+
const baseIsObject = base && typeof base === 'object' && !Array.isArray(base);
|
|
154
|
+
const overrideIsObject = override && typeof override === 'object' && !Array.isArray(override);
|
|
155
|
+
if (!overrideIsObject) {
|
|
156
|
+
return override === undefined ? base : override;
|
|
157
|
+
}
|
|
158
|
+
const out = baseIsObject ? { ...base } : {};
|
|
159
|
+
for (const [key, value] of Object.entries(override)) {
|
|
160
|
+
const baseValue = baseIsObject ? base[key] : undefined;
|
|
161
|
+
const bothObjects = baseValue && typeof baseValue === 'object' && !Array.isArray(baseValue) &&
|
|
162
|
+
value && typeof value === 'object' && !Array.isArray(value);
|
|
163
|
+
out[key] = bothObjects ? deepMerge(baseValue, value) : value;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
18
168
|
const DEFAULT_CONFIG = {
|
|
19
|
-
|
|
20
|
-
|
|
169
|
+
onboarding: {
|
|
170
|
+
version: 2,
|
|
171
|
+
step: 'not_started', // not_started|tiers|ingress|verify|complete
|
|
172
|
+
tiers_confirmed: false,
|
|
173
|
+
ingress_confirmed: false,
|
|
174
|
+
verify_confirmed: false,
|
|
175
|
+
last_run_at: null
|
|
176
|
+
},
|
|
21
177
|
|
|
22
178
|
// Permission tiers
|
|
23
179
|
tiers: {
|
|
@@ -25,7 +181,7 @@ const DEFAULT_CONFIG = {
|
|
|
25
181
|
name: 'Public',
|
|
26
182
|
description: 'Basic networking - safe for anyone',
|
|
27
183
|
capabilities: ['context-read'],
|
|
28
|
-
topics: [],
|
|
184
|
+
topics: ['chat'],
|
|
29
185
|
goals: [],
|
|
30
186
|
disclosure: 'minimal',
|
|
31
187
|
examples: ['calendar availability', 'public social posts', 'general questions']
|
|
@@ -34,19 +190,19 @@ const DEFAULT_CONFIG = {
|
|
|
34
190
|
name: 'Friends',
|
|
35
191
|
description: 'Most capabilities, no sensitive financial data',
|
|
36
192
|
capabilities: ['context-read', 'calendar.read', 'email.read', 'search'],
|
|
37
|
-
topics: [],
|
|
193
|
+
topics: ['chat', 'search', 'openclaw', 'a2a'],
|
|
38
194
|
goals: [],
|
|
39
195
|
disclosure: 'public',
|
|
40
196
|
examples: ['email summaries', 'schedule meetings', 'project discussions']
|
|
41
197
|
},
|
|
42
|
-
|
|
43
|
-
name: '
|
|
44
|
-
description: 'Full access - only for
|
|
198
|
+
family: {
|
|
199
|
+
name: 'Family',
|
|
200
|
+
description: 'Full access - only for your inner circle',
|
|
45
201
|
capabilities: ['context-read', 'calendar', 'email', 'search', 'tools', 'memory'],
|
|
46
|
-
topics: [],
|
|
202
|
+
topics: ['chat', 'search', 'openclaw', 'a2a', 'tools', 'memory'],
|
|
47
203
|
goals: [],
|
|
48
204
|
disclosure: 'public',
|
|
49
|
-
examples: ['
|
|
205
|
+
examples: ['deep collaboration', 'private project context', 'personal notes']
|
|
50
206
|
},
|
|
51
207
|
custom: {
|
|
52
208
|
name: 'Custom',
|
|
@@ -99,7 +255,7 @@ class A2AConfig {
|
|
|
99
255
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
100
256
|
try {
|
|
101
257
|
const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
102
|
-
return
|
|
258
|
+
return deepMerge(JSON.parse(JSON.stringify(DEFAULT_CONFIG)), saved);
|
|
103
259
|
} catch (e) {
|
|
104
260
|
logger.error('A2A config is corrupted, using defaults', {
|
|
105
261
|
event: 'a2a_config_corrupt',
|
|
@@ -110,10 +266,10 @@ class A2AConfig {
|
|
|
110
266
|
config_file: CONFIG_FILE
|
|
111
267
|
}
|
|
112
268
|
});
|
|
113
|
-
return
|
|
269
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
114
270
|
}
|
|
115
271
|
}
|
|
116
|
-
return
|
|
272
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
117
273
|
}
|
|
118
274
|
|
|
119
275
|
_save() {
|
|
@@ -133,18 +289,48 @@ class A2AConfig {
|
|
|
133
289
|
|
|
134
290
|
// Check if onboarding is complete
|
|
135
291
|
isOnboarded() {
|
|
136
|
-
return this.config.
|
|
292
|
+
return this.config.onboarding &&
|
|
293
|
+
this.config.onboarding.version === 2 &&
|
|
294
|
+
this.config.onboarding.step === 'complete';
|
|
137
295
|
}
|
|
138
296
|
|
|
139
297
|
// Mark onboarding complete
|
|
140
298
|
completeOnboarding() {
|
|
141
|
-
this.config.
|
|
299
|
+
this.config.onboarding = this.config.onboarding || {};
|
|
300
|
+
this.config.onboarding.version = 2;
|
|
301
|
+
this.config.onboarding.step = 'complete';
|
|
302
|
+
this.config.onboarding.tiers_confirmed = true;
|
|
303
|
+
this.config.onboarding.ingress_confirmed = true;
|
|
304
|
+
this.config.onboarding.verify_confirmed = true;
|
|
305
|
+
this.config.onboarding.last_run_at = new Date().toISOString();
|
|
142
306
|
this._save();
|
|
143
307
|
}
|
|
144
308
|
|
|
145
309
|
// Reset to run onboarding again
|
|
146
310
|
resetOnboarding() {
|
|
147
|
-
this.config.
|
|
311
|
+
this.config.onboarding = this.config.onboarding || {};
|
|
312
|
+
this.config.onboarding.version = 2;
|
|
313
|
+
this.config.onboarding.step = 'not_started';
|
|
314
|
+
this.config.onboarding.tiers_confirmed = false;
|
|
315
|
+
this.config.onboarding.ingress_confirmed = false;
|
|
316
|
+
this.config.onboarding.verify_confirmed = false;
|
|
317
|
+
this.config.onboarding.last_run_at = new Date().toISOString();
|
|
318
|
+
this._save();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
getOnboarding() {
|
|
322
|
+
const ob = (this.config && this.config.onboarding && typeof this.config.onboarding === 'object')
|
|
323
|
+
? this.config.onboarding
|
|
324
|
+
: {};
|
|
325
|
+
return deepMerge(DEFAULT_CONFIG.onboarding, ob);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
setOnboarding(patch = {}) {
|
|
329
|
+
const current = this.getOnboarding();
|
|
330
|
+
const merged = deepMerge(current, patch);
|
|
331
|
+
merged.version = 2;
|
|
332
|
+
merged.last_run_at = new Date().toISOString();
|
|
333
|
+
this.config.onboarding = merged;
|
|
148
334
|
this._save();
|
|
149
335
|
}
|
|
150
336
|
|
|
@@ -154,7 +340,19 @@ class A2AConfig {
|
|
|
154
340
|
}
|
|
155
341
|
|
|
156
342
|
setTier(tierName, tierConfig) {
|
|
157
|
-
|
|
343
|
+
const id = String(tierName || '').trim();
|
|
344
|
+
if (!id) {
|
|
345
|
+
throw configValidationError(
|
|
346
|
+
'A2A_CONFIG_INVALID_TIER_ID',
|
|
347
|
+
'Tier name is required',
|
|
348
|
+
'Use one of: public|friends|family|custom (or a non-empty custom tier id).'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const patch = validateTierPatch(id, tierConfig);
|
|
353
|
+
this.config.tiers = isPlainObject(this.config.tiers) ? this.config.tiers : {};
|
|
354
|
+
const existing = isPlainObject(this.config.tiers[id]) ? this.config.tiers[id] : {};
|
|
355
|
+
this.config.tiers[id] = { ...existing, ...patch };
|
|
158
356
|
this._save();
|
|
159
357
|
}
|
|
160
358
|
|
package/src/lib/conversations.js
CHANGED
|
@@ -47,6 +47,7 @@ class ConversationStore {
|
|
|
47
47
|
// Best effort - ignore on platforms without chmod support.
|
|
48
48
|
}
|
|
49
49
|
this._migrate();
|
|
50
|
+
this._ensureLatestSchema(Database);
|
|
50
51
|
return this.db;
|
|
51
52
|
} catch (err) {
|
|
52
53
|
if (err.code === 'MODULE_NOT_FOUND') {
|
|
@@ -128,6 +129,79 @@ class ConversationStore {
|
|
|
128
129
|
`);
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
_ensureLatestSchema(Database) {
|
|
133
|
+
// Prototype-mode stance: do not attempt in-place migrations.
|
|
134
|
+
// If schema is missing required columns, back up and recreate the DB.
|
|
135
|
+
try {
|
|
136
|
+
const info = this.db.prepare('PRAGMA table_info(conversations)').all();
|
|
137
|
+
if (!Array.isArray(info) || info.length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const cols = new Set(info.map(row => row && row.name).filter(Boolean));
|
|
141
|
+
const required = [
|
|
142
|
+
'joint_action_items',
|
|
143
|
+
'collaboration_opportunity'
|
|
144
|
+
];
|
|
145
|
+
const missing = required.filter(c => !cols.has(c));
|
|
146
|
+
if (missing.length === 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const backupPath = `${this.dbPath}.bak.${Date.now()}`;
|
|
151
|
+
logger.warn('Conversation DB schema mismatch; resetting to latest schema', {
|
|
152
|
+
event: 'conversation_db_schema_reset',
|
|
153
|
+
data: {
|
|
154
|
+
db_path: this.dbPath,
|
|
155
|
+
backup_path: backupPath,
|
|
156
|
+
missing_columns: missing
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
try { this.db.close(); } catch (_) {}
|
|
161
|
+
this.db = null;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
fs.renameSync(this.dbPath, backupPath);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
try {
|
|
167
|
+
fs.copyFileSync(this.dbPath, backupPath);
|
|
168
|
+
fs.unlinkSync(this.dbPath);
|
|
169
|
+
} catch (err2) {
|
|
170
|
+
// If we can't move the old DB out of the way, keep going without resetting.
|
|
171
|
+
logger.error('Failed to back up conversations DB for schema reset', {
|
|
172
|
+
event: 'conversation_db_schema_reset_backup_failed',
|
|
173
|
+
error: err2,
|
|
174
|
+
error_code: 'CONVERSATION_DB_SCHEMA_RESET_BACKUP_FAILED',
|
|
175
|
+
hint: 'Check file permissions on a2a-conversations.db and ensure the process can rename/unlink it.',
|
|
176
|
+
data: {
|
|
177
|
+
db_path: this.dbPath,
|
|
178
|
+
backup_path: backupPath
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
const reopen = new Database(this.dbPath);
|
|
182
|
+
this.db = reopen;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.db = new Database(this.dbPath);
|
|
188
|
+
try {
|
|
189
|
+
fs.chmodSync(this.dbPath, 0o600);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// Best effort.
|
|
192
|
+
}
|
|
193
|
+
this._migrate();
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// Best effort: leave existing DB in place if schema validation fails unexpectedly.
|
|
196
|
+
logger.error('Conversation DB schema validation failed', {
|
|
197
|
+
event: 'conversation_db_schema_validation_failed',
|
|
198
|
+
error: err,
|
|
199
|
+
error_code: 'CONVERSATION_DB_SCHEMA_VALIDATION_FAILED',
|
|
200
|
+
hint: 'If this persists, delete ~/.config/openclaw/a2a-conversations.db (prototype mode) and restart.'
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
131
205
|
/**
|
|
132
206
|
* Generate a conversation ID (shared between both agents)
|
|
133
207
|
*/
|
package/src/lib/disclosure.js
CHANGED
|
@@ -146,7 +146,7 @@ function generateDefaultManifest(contextFiles = {}) {
|
|
|
146
146
|
const soulContent = contextFiles.soul || '';
|
|
147
147
|
|
|
148
148
|
const hasContent = userContent || heartbeatContent || soulContent ||
|
|
149
|
-
contextFiles.
|
|
149
|
+
contextFiles.memory || contextFiles.claude;
|
|
150
150
|
|
|
151
151
|
if (!hasContent) {
|
|
152
152
|
// Minimal starter manifest
|
|
@@ -263,21 +263,6 @@ function generateDefaultManifest(contextFiles = {}) {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
// Extract capability keywords from SKILL.md and installed skills
|
|
267
|
-
const skillContent = (contextFiles.skill || '') + '\n' + (contextFiles.skills || '');
|
|
268
|
-
if (skillContent.trim()) {
|
|
269
|
-
const capabilityLines = skillContent
|
|
270
|
-
.split('\n')
|
|
271
|
-
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
272
|
-
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
273
|
-
.filter(l => l.length > 5 && l.length < 120)
|
|
274
|
-
.slice(0, 6);
|
|
275
|
-
|
|
276
|
-
capabilityLines.forEach(cap => {
|
|
277
|
-
manifest.topics.public.discuss_freely.push({ topic: cap.slice(0, 60), detail: cap });
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
266
|
// Extract topic keywords from memory files
|
|
282
267
|
if (contextFiles.memory) {
|
|
283
268
|
const memoryLines = contextFiles.memory
|
|
@@ -350,7 +335,8 @@ function readContextFiles(workspaceDir) {
|
|
|
350
335
|
heartbeat: read('HEARTBEAT.md'),
|
|
351
336
|
soul: read('SOUL.md'),
|
|
352
337
|
skill: read('SKILL.md'),
|
|
353
|
-
claude: read('CLAUDE.md')
|
|
338
|
+
claude: read('CLAUDE.md'),
|
|
339
|
+
skills: ''
|
|
354
340
|
};
|
|
355
341
|
|
|
356
342
|
// Scan memory/*.md
|
|
@@ -366,32 +352,6 @@ function readContextFiles(workspaceDir) {
|
|
|
366
352
|
} catch (e) {}
|
|
367
353
|
}
|
|
368
354
|
|
|
369
|
-
// Scan installed skills from both OpenClaw and standalone paths
|
|
370
|
-
const homeDir = process.env.HOME || '/tmp';
|
|
371
|
-
const skillsDirs = [
|
|
372
|
-
process.env.OPENCLAW_SKILLS || path.join(homeDir, '.openclaw', 'skills'),
|
|
373
|
-
path.join(CONFIG_DIR, 'skills')
|
|
374
|
-
];
|
|
375
|
-
const skillFragments = [];
|
|
376
|
-
const seenSkillDirs = new Set();
|
|
377
|
-
for (const skillsDir of skillsDirs) {
|
|
378
|
-
if (!fs.existsSync(skillsDir) || seenSkillDirs.has(skillsDir)) continue;
|
|
379
|
-
seenSkillDirs.add(skillsDir);
|
|
380
|
-
try {
|
|
381
|
-
const dirs = fs.readdirSync(skillsDir).filter(d => {
|
|
382
|
-
try { return fs.statSync(path.join(skillsDir, d)).isDirectory(); }
|
|
383
|
-
catch (e) { return false; }
|
|
384
|
-
});
|
|
385
|
-
for (const d of dirs) {
|
|
386
|
-
try {
|
|
387
|
-
const content = fs.readFileSync(path.join(skillsDir, d, 'SKILL.md'), 'utf8');
|
|
388
|
-
if (content) skillFragments.push(content);
|
|
389
|
-
} catch (e) {}
|
|
390
|
-
}
|
|
391
|
-
} catch (e) {}
|
|
392
|
-
}
|
|
393
|
-
result.skills = skillFragments.join('\n---\n');
|
|
394
|
-
|
|
395
355
|
return result;
|
|
396
356
|
}
|
|
397
357
|
|
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
|
@@ -166,24 +166,23 @@ async function resolveInviteHost(options = {}) {
|
|
|
166
166
|
options.refreshExternalIp && isPublicIpHostname(parsed.hostname)
|
|
167
167
|
);
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
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) {
|
|
187
186
|
const finalHost = formatHostPort(external.ip, desiredPort);
|
|
188
187
|
if (finalHost !== candidateHostWithPort) {
|
|
189
188
|
warnings.push(
|
|
@@ -195,17 +194,21 @@ async function resolveInviteHost(options = {}) {
|
|
|
195
194
|
source: 'external_ip',
|
|
196
195
|
originalHost: candidateHostWithPort,
|
|
197
196
|
externalIp: external.ip,
|
|
197
|
+
externalIpInfo: external,
|
|
198
198
|
warnings
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
}
|
|
205
207
|
return {
|
|
206
208
|
host: candidateHostWithPort,
|
|
207
209
|
source: candidateSource,
|
|
208
210
|
originalHost: candidateHostWithPort,
|
|
211
|
+
externalIpInfo: external,
|
|
209
212
|
warnings
|
|
210
213
|
};
|
|
211
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() {
|