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 +7 -5
- package/bin/cli.js +148 -76
- package/package.json +1 -1
- package/src/lib/config.js +147 -1
- package/src/lib/conversations.js +74 -0
- package/src/lib/disclosure.js +3 -43
- package/src/routes/dashboard.js +48 -12
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
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
|
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
|
-
--
|
|
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
|
|
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
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
|
-
|
|
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
|
|
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/routes/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
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]) {
|