a2acalling 0.6.2 → 0.6.4

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/SKILL.md CHANGED
@@ -96,7 +96,7 @@ Deterministic onboarding flow (sequential, flags-based):
96
96
 
97
97
  1. Background bootstrap (config + disclosure)
98
98
  2. Owner dashboard access (local URL + optional Callbook Remote install link)
99
- 3. Set permission tiers: populate and confirm tier `topics` + `goals`
99
+ 3. Set permission tiers: populate tier `topics` + `goals` (schema-validated and saved)
100
100
  4. Port scan + reverse proxy guidance (if needed for public hostname)
101
101
  5. External IP confirmation and public reachability check (public hostname only)
102
102
 
@@ -110,12 +110,14 @@ a2a quickstart --port 3001
110
110
  a2a quickstart --hostname YOUR_DOMAIN:443 --port 3001
111
111
  ```
112
112
 
113
- Quickstart will print a proposed tier configuration. After the user reviews it, re-run with:
113
+ Quickstart prints and saves a tier configuration immediately (validated by the config layer). If you want to override the Friends tier topics/interests, rerun with:
114
114
 
115
115
  ```bash
116
- a2a quickstart --port 3001 --confirm-tiers
117
- # or (public hostname)
118
- a2a quickstart --hostname YOUR_DOMAIN:443 --port 3001 --confirm-tiers
116
+ # Provide topics directly
117
+ a2a quickstart --port 3001 --friends-topics "chat,search,openclaw,a2a"
118
+
119
+ # Or prompt interactively for Friends tier topics
120
+ a2a quickstart --port 3001 --interactive
119
121
  ```
120
122
 
121
123
  If reverse proxy/ingress is required, Quickstart will stop and ask for explicit confirmation (`--confirm-ingress`).
package/bin/cli.js CHANGED
@@ -983,12 +983,21 @@ https://github.com/onthegonow/a2a_calling`;
983
983
  }
984
984
  }
985
985
 
986
- async function externalPingCheck(targetUrl) {
987
- const providers = [
988
- {
989
- name: 'allorigins',
990
- buildUrl: () => {
991
- const u = new URL('https://api.allorigins.win/raw');
986
+ async function externalPingCheck(targetUrl) {
987
+ // Try direct access first. In practice this is the most reliable signal:
988
+ // it avoids flaky third-party proxies and catches obvious scheme/port mistakes.
989
+ try {
990
+ const direct = await fetchUrlText(targetUrl, 2500);
991
+ return { ok: looksLikePong(direct.body), provider: 'direct', statusCode: direct.statusCode };
992
+ } catch (err) {
993
+ // Fall back to remote fetch providers below.
994
+ }
995
+
996
+ const providers = [
997
+ {
998
+ name: 'allorigins',
999
+ buildUrl: () => {
1000
+ const u = new URL('https://api.allorigins.win/raw');
992
1001
  u.searchParams.set('url', targetUrl);
993
1002
  return u.toString();
994
1003
  }
@@ -1105,19 +1114,26 @@ https://github.com/onthegonow/a2a_calling`;
1105
1114
  // ── Step 1: Background bootstrap (config + manifest) ─────────
1106
1115
  let contextFiles = {};
1107
1116
  let manifest = {};
1108
- try {
1109
- contextFiles = disc.readContextFiles(workspaceDir);
1110
- manifest = disc.loadManifest();
1111
- if (!manifest || Object.keys(manifest).length === 0) {
1112
- const generated = disc.generateDefaultManifest(contextFiles);
1113
- disc.saveManifest(generated);
1114
- manifest = generated;
1115
- }
1116
- } catch (err) {
1117
- // Non-fatal: onboarding can proceed even if manifest fails.
1118
- contextFiles = {};
1119
- manifest = {};
1120
- }
1117
+ try {
1118
+ contextFiles = disc.readContextFiles(workspaceDir);
1119
+ const forceManifest = Boolean(args.flags.force || args.flags['regen-manifest'] || args.flags.regenManifest);
1120
+ if (forceManifest) {
1121
+ const generated = disc.generateDefaultManifest(contextFiles);
1122
+ disc.saveManifest(generated);
1123
+ manifest = generated;
1124
+ } else {
1125
+ manifest = disc.loadManifest();
1126
+ if (!manifest || Object.keys(manifest).length === 0) {
1127
+ const generated = disc.generateDefaultManifest(contextFiles);
1128
+ disc.saveManifest(generated);
1129
+ manifest = generated;
1130
+ }
1131
+ }
1132
+ } catch (err) {
1133
+ // Non-fatal: onboarding can proceed even if manifest fails.
1134
+ contextFiles = {};
1135
+ manifest = {};
1136
+ }
1121
1137
 
1122
1138
  console.log('\nA2A deterministic onboarding');
1123
1139
  console.log('──────────────────────────');
@@ -1180,32 +1196,84 @@ https://github.com/onthegonow/a2a_calling`;
1180
1196
  }
1181
1197
  }
1182
1198
 
1183
- // ── Step 3: Permission tiers (topics + goals) ───────────────
1184
- const onboardingAfterAccess = config.getOnboarding();
1185
- if (!onboardingAfterAccess.tiers_confirmed) {
1186
- const recommendations = buildTierRecommendations(contextFiles, manifest);
1199
+ // ── Step 3: Permission tiers (topics + goals) ───────────────
1200
+ const onboardingAfterAccess = config.getOnboarding();
1201
+ if (!onboardingAfterAccess.tiers_confirmed) {
1202
+ const recommendations = buildTierRecommendations(contextFiles, manifest);
1203
+
1204
+ const parseFreeTextList = (raw) => {
1205
+ if (raw === undefined || raw === null || raw === true) return [];
1206
+ const text = String(raw || '').trim();
1207
+ if (!text) return [];
1208
+ return text
1209
+ .split(/[\n,]+/g)
1210
+ .map(s => s.trim())
1211
+ .filter(Boolean);
1212
+ };
1213
+
1214
+ const promptLine = async (question) => {
1215
+ const readline = require('readline');
1216
+ return await new Promise(resolve => {
1217
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1218
+ rl.question(question, (answer) => {
1219
+ rl.close();
1220
+ resolve(String(answer || '').trim());
1221
+ });
1222
+ });
1223
+ };
1224
+
1225
+ // Optional owner override: Friends tier topics/interests (most important tier).
1226
+ const interactive = Boolean(
1227
+ args.flags.interactive ||
1228
+ args.flags['ask-friends-topics'] ||
1229
+ args.flags.askFriendsTopics
1230
+ );
1231
+ let friendsTopicsOverride = parseFreeTextList(args.flags['friends-topics'] || args.flags.friendsTopics);
1232
+ const noWorkspaceContext = !contextFiles.user && !contextFiles.heartbeat && !contextFiles.soul &&
1233
+ !contextFiles.memory && !contextFiles.claude;
1234
+ const shouldPromptFriendsTopics = (interactive || noWorkspaceContext) &&
1235
+ friendsTopicsOverride.length === 0 &&
1236
+ process.stdin.isTTY &&
1237
+ process.stdout.isTTY;
1238
+ if (shouldPromptFriendsTopics) {
1239
+ const suggested = (recommendations.friends.topics || []).slice(0, 12).join(', ');
1240
+ const answer = await promptLine(`Friends-tier topics/interests (comma-separated).\nSuggested: ${suggested}\n> `);
1241
+ friendsTopicsOverride = parseFreeTextList(answer);
1242
+ }
1187
1243
 
1188
- config.setTier('public', recommendations.public);
1189
- config.setTier('friends', recommendations.friends);
1190
- config.setTier('family', recommendations.family);
1244
+ if (friendsTopicsOverride.length > 0) {
1245
+ const normalized = uniqueNonEmpty(friendsTopicsOverride.map(slugify).filter(Boolean), 24);
1246
+ recommendations.friends.topics = uniqueNonEmpty(
1247
+ [...(recommendations.public.topics || []), ...normalized],
1248
+ 24
1249
+ );
1250
+ recommendations.family.topics = uniqueNonEmpty(
1251
+ [...(recommendations.friends.topics || []), ...(recommendations.family.topics || [])],
1252
+ 30
1253
+ );
1254
+ }
1191
1255
 
1192
- printTierSummary(recommendations);
1256
+ try {
1257
+ config.setTier('public', recommendations.public);
1258
+ config.setTier('friends', recommendations.friends);
1259
+ config.setTier('family', recommendations.family);
1260
+ } catch (err) {
1261
+ console.error('\n❌ Tier configuration validation failed.');
1262
+ console.error(` ${err.message}`);
1263
+ if (err.hint) {
1264
+ console.error(` Hint: ${err.hint}`);
1265
+ }
1266
+ console.error('');
1267
+ process.exit(1);
1268
+ }
1193
1269
 
1194
- if (!args.flags['confirm-tiers']) {
1195
- console.log('3️⃣ Confirm tiers');
1196
- console.log('Review the topics/goals above. To confirm and continue, run:');
1197
- console.log(` a2a quickstart --port ${backendPort} --confirm-tiers`);
1198
- console.log('Optional (remote access):');
1199
- console.log(` a2a quickstart --hostname YOUR_DOMAIN:443 --port ${backendPort} --confirm-tiers`);
1200
- console.log('');
1201
- return;
1202
- }
1270
+ printTierSummary(recommendations);
1203
1271
 
1204
- config.setOnboarding({
1205
- step: 'tiers',
1206
- tiers_confirmed: true
1207
- });
1208
- }
1272
+ config.setOnboarding({
1273
+ step: 'tiers',
1274
+ tiers_confirmed: true
1275
+ });
1276
+ }
1209
1277
 
1210
1278
  // ── Step 4: Port scan + reverse proxy guidance (if needed) ──
1211
1279
  console.log('\n4️⃣ Port scan + reverse proxy');
@@ -1239,13 +1307,13 @@ https://github.com/onthegonow/a2a_calling`;
1239
1307
  console.log(` /api/a2a/* -> http://127.0.0.1:${backendPort}`);
1240
1308
  console.log(` /dashboard/* -> http://127.0.0.1:${backendPort}`);
1241
1309
  console.log(` /callbook/* -> http://127.0.0.1:${backendPort}`);
1242
- console.log('');
1243
- console.log('If you have configured your reverse proxy and want to continue, run:');
1244
- console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-tiers --confirm-ingress`);
1245
- console.log('');
1246
- if (!args.flags['confirm-ingress']) {
1247
- return;
1248
- }
1310
+ console.log('');
1311
+ console.log('If you have configured your reverse proxy and want to continue, run:');
1312
+ console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
1313
+ console.log('');
1314
+ if (!args.flags['confirm-ingress']) {
1315
+ return;
1316
+ }
1249
1317
  } else {
1250
1318
  console.log('✅ No reverse proxy required based on invite host/port.');
1251
1319
  }
@@ -1293,22 +1361,25 @@ https://github.com/onthegonow/a2a_calling`;
1293
1361
  }
1294
1362
  }
1295
1363
 
1296
- if (inviteLooksLocal) {
1297
- console.log('Skipping external reachability check: invite host looks local/unroutable.');
1298
- } else {
1299
- const extPing = await externalPingCheck(expectedPingUrl);
1300
- if (extPing.ok) {
1301
- console.log(`✅ External ping OK (${extPing.provider})`);
1302
- } else if (!args.flags['skip-verify']) {
1303
- console.log('⚠️ External ping FAILED (server may not be publicly reachable yet).');
1304
- console.log('Fix ingress (DNS/reverse proxy/firewall), then rerun with:');
1305
- console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-tiers --confirm-ingress`);
1306
- console.log('');
1307
- return;
1308
- } else {
1309
- console.log('⚠️ External ping FAILED (skipped via --skip-verify).');
1310
- }
1311
- }
1364
+ if (inviteLooksLocal) {
1365
+ console.log('Skipping external reachability check: invite host looks local/unroutable.');
1366
+ } else {
1367
+ const extPing = await externalPingCheck(expectedPingUrl);
1368
+ if (extPing.ok) {
1369
+ console.log(`✅ External ping OK (${extPing.provider})`);
1370
+ } else if (args.flags['skip-verify']) {
1371
+ console.log('⚠️ External ping FAILED (skipped via --skip-verify).');
1372
+ } else if (args.flags['confirm-ingress']) {
1373
+ console.log('⚠️ External ping FAILED (continuing due to --confirm-ingress).');
1374
+ } else {
1375
+ console.log('⚠️ External ping FAILED (server may not be publicly reachable yet).');
1376
+ console.log('Fix ingress (DNS/reverse proxy/firewall), then rerun. If you want to proceed anyway:');
1377
+ console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
1378
+ console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --skip-verify`);
1379
+ console.log('');
1380
+ return;
1381
+ }
1382
+ }
1312
1383
 
1313
1384
  if (!config.getOnboarding().verify_confirmed) {
1314
1385
  config.setOnboarding({
@@ -1453,15 +1524,14 @@ https://github.com/onthegonow/a2a_calling`;
1453
1524
 
1454
1525
  const contextFiles = readContextFiles(workspaceDir);
1455
1526
  // Print what was found
1456
- const sources = {
1457
- 'USER.md': contextFiles.user,
1458
- 'HEARTBEAT.md': contextFiles.heartbeat,
1459
- 'SOUL.md': contextFiles.soul,
1460
- 'SKILL.md': contextFiles.skill,
1461
- 'CLAUDE.md': contextFiles.claude,
1462
- 'memory/*.md': contextFiles.memory,
1463
- 'Installed skills': contextFiles.skills
1464
- };
1527
+ const sources = {
1528
+ 'USER.md': contextFiles.user,
1529
+ 'HEARTBEAT.md': contextFiles.heartbeat,
1530
+ 'SOUL.md': contextFiles.soul,
1531
+ 'SKILL.md': contextFiles.skill,
1532
+ 'CLAUDE.md': contextFiles.claude,
1533
+ 'memory/*.md': contextFiles.memory
1534
+ };
1465
1535
  for (const [name, content] of Object.entries(sources)) {
1466
1536
  console.log(` ${content ? '\u2705' : '\u274c'} ${name}`);
1467
1537
  }
@@ -1537,14 +1607,16 @@ Server:
1537
1607
  server Start the A2A server
1538
1608
  --port, -p Port to listen on (default: 3001)
1539
1609
 
1540
- quickstart Deterministic onboarding (access → tiers → ingress → verify)
1610
+ quickstart Onboarding (access → tiers → ingress → verify)
1541
1611
  --hostname Public hostname for remote access (e.g. myserver.com:443)
1542
1612
  --public-port Port to assume when --hostname omits a port (default: 443)
1543
1613
  --port A2A server port to run locally (default: 3001)
1544
- --confirm-tiers Confirm tier topics/goals and continue
1614
+ --friends-topics Override Friends tier topics/interests (comma or newline-separated)
1615
+ --interactive Prompt for Friends tier topics if needed
1545
1616
  --confirm-ingress Confirm reverse proxy/ingress is configured and continue
1546
1617
  --skip-verify Skip external reachability check (not recommended)
1547
- --force Reset onboarding and start over
1618
+ --force Reset onboarding + regenerate disclosure manifest
1619
+ --regen-manifest Regenerate disclosure manifest (no onboarding reset)
1548
1620
 
1549
1621
  onboard Generate disclosure manifest from workspace context
1550
1622
  --force Re-run even if already onboarded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/lib/config.js CHANGED
@@ -15,6 +15,140 @@ 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
+
18
152
  function deepMerge(base, override) {
19
153
  const baseIsObject = base && typeof base === 'object' && !Array.isArray(base);
20
154
  const overrideIsObject = override && typeof override === 'object' && !Array.isArray(override);
@@ -206,7 +340,19 @@ class A2AConfig {
206
340
  }
207
341
 
208
342
  setTier(tierName, tierConfig) {
209
- this.config.tiers[tierName] = { ...this.config.tiers[tierName], ...tierConfig };
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 };
210
356
  this._save();
211
357
  }
212
358
 
@@ -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
  */
@@ -146,7 +146,7 @@ function generateDefaultManifest(contextFiles = {}) {
146
146
  const soulContent = contextFiles.soul || '';
147
147
 
148
148
  const hasContent = userContent || heartbeatContent || soulContent ||
149
- contextFiles.skill || contextFiles.memory || contextFiles.skills || contextFiles.claude;
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
 
@@ -1069,7 +1069,16 @@ function createDashboardApiRouter(options = {}) {
1069
1069
  if (body.topics !== undefined) update.topics = sanitizeStringArray(body.topics, 200, 160);
1070
1070
  if (body.goals !== undefined) update.goals = sanitizeStringArray(body.goals, 200, 160);
1071
1071
 
1072
- context.config.setTier(tierId, update);
1072
+ try {
1073
+ context.config.setTier(tierId, update);
1074
+ } catch (err) {
1075
+ return res.status(400).json({
1076
+ success: false,
1077
+ error: 'invalid_tier_config',
1078
+ code: err.code || 'A2A_CONFIG_INVALID_TIER_CONFIG',
1079
+ message: err.message
1080
+ });
1081
+ }
1073
1082
 
1074
1083
  if (body.manifest) {
1075
1084
  const manifest = loadManifest();
@@ -1099,17 +1108,35 @@ function createDashboardApiRouter(options = {}) {
1099
1108
 
1100
1109
  const copyFrom = normalizeTierId(body.copy_from || '');
1101
1110
  if (copyFrom && cfg.tiers && cfg.tiers[copyFrom]) {
1102
- context.config.setTier(tierId, { ...cfg.tiers[copyFrom] });
1111
+ try {
1112
+ context.config.setTier(tierId, { ...cfg.tiers[copyFrom] });
1113
+ } catch (err) {
1114
+ return res.status(400).json({
1115
+ success: false,
1116
+ error: 'invalid_tier_config',
1117
+ code: err.code || 'A2A_CONFIG_INVALID_TIER_CONFIG',
1118
+ message: err.message
1119
+ });
1120
+ }
1103
1121
  } else {
1104
- context.config.setTier(tierId, {
1105
- name: sanitizeString(body.name || tierId, 120),
1106
- description: sanitizeString(body.description || 'Custom tier', 300),
1107
- capabilities: sanitizeStringArray(body.capabilities || []),
1108
- topics: sanitizeStringArray(body.topics || []),
1109
- goals: sanitizeStringArray(body.goals || []),
1110
- disclosure: sanitizeString(body.disclosure || 'minimal', 40),
1111
- examples: sanitizeStringArray(body.examples || [], 20, 120)
1112
- });
1122
+ try {
1123
+ context.config.setTier(tierId, {
1124
+ name: sanitizeString(body.name || tierId, 120),
1125
+ description: sanitizeString(body.description || 'Custom tier', 300),
1126
+ capabilities: sanitizeStringArray(body.capabilities || []),
1127
+ topics: sanitizeStringArray(body.topics || []),
1128
+ goals: sanitizeStringArray(body.goals || []),
1129
+ disclosure: sanitizeString(body.disclosure || 'minimal', 40),
1130
+ examples: sanitizeStringArray(body.examples || [], 20, 120)
1131
+ });
1132
+ } catch (err) {
1133
+ return res.status(400).json({
1134
+ success: false,
1135
+ error: 'invalid_tier_config',
1136
+ code: err.code || 'A2A_CONFIG_INVALID_TIER_CONFIG',
1137
+ message: err.message
1138
+ });
1139
+ }
1113
1140
  }
1114
1141
 
1115
1142
  return res.json({ success: true, tier_id: tierId });
@@ -1127,7 +1154,16 @@ function createDashboardApiRouter(options = {}) {
1127
1154
  return res.status(404).json({ success: false, error: 'source_tier_not_found' });
1128
1155
  }
1129
1156
 
1130
- context.config.setTier(toTier, { ...cfg.tiers[fromTier] });
1157
+ try {
1158
+ context.config.setTier(toTier, { ...cfg.tiers[fromTier] });
1159
+ } catch (err) {
1160
+ return res.status(400).json({
1161
+ success: false,
1162
+ error: 'invalid_tier_config',
1163
+ code: err.code || 'A2A_CONFIG_INVALID_TIER_CONFIG',
1164
+ message: err.message
1165
+ });
1166
+ }
1131
1167
 
1132
1168
  const manifest = loadManifest();
1133
1169
  if (manifest.topics && manifest.topics[fromTier]) {