a2acalling 0.6.3 → 0.6.5
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/bin/cli.js +98 -57
- package/package.json +1 -1
- package/scripts/install-openclaw.js +2 -3
- package/src/lib/disclosure.js +198 -155
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
|
}
|
|
@@ -1109,13 +1118,15 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1109
1118
|
contextFiles = disc.readContextFiles(workspaceDir);
|
|
1110
1119
|
const forceManifest = Boolean(args.flags.force || args.flags['regen-manifest'] || args.flags.regenManifest);
|
|
1111
1120
|
if (forceManifest) {
|
|
1112
|
-
|
|
1121
|
+
// Force-regen uses minimal starter; agent-driven extraction is the
|
|
1122
|
+
// proper way to populate topics (via `a2a onboard --submit`).
|
|
1123
|
+
const generated = disc.generateDefaultManifest();
|
|
1113
1124
|
disc.saveManifest(generated);
|
|
1114
1125
|
manifest = generated;
|
|
1115
1126
|
} else {
|
|
1116
1127
|
manifest = disc.loadManifest();
|
|
1117
1128
|
if (!manifest || Object.keys(manifest).length === 0) {
|
|
1118
|
-
const generated = disc.generateDefaultManifest(
|
|
1129
|
+
const generated = disc.generateDefaultManifest();
|
|
1119
1130
|
disc.saveManifest(generated);
|
|
1120
1131
|
manifest = generated;
|
|
1121
1132
|
}
|
|
@@ -1352,22 +1363,25 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1352
1363
|
}
|
|
1353
1364
|
}
|
|
1354
1365
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
} else if (
|
|
1366
|
+
if (inviteLooksLocal) {
|
|
1367
|
+
console.log('Skipping external reachability check: invite host looks local/unroutable.');
|
|
1368
|
+
} else {
|
|
1369
|
+
const extPing = await externalPingCheck(expectedPingUrl);
|
|
1370
|
+
if (extPing.ok) {
|
|
1371
|
+
console.log(`✅ External ping OK (${extPing.provider})`);
|
|
1372
|
+
} else if (args.flags['skip-verify']) {
|
|
1373
|
+
console.log('⚠️ External ping FAILED (skipped via --skip-verify).');
|
|
1374
|
+
} else if (args.flags['confirm-ingress']) {
|
|
1375
|
+
console.log('⚠️ External ping FAILED (continuing due to --confirm-ingress).');
|
|
1376
|
+
} else {
|
|
1362
1377
|
console.log('⚠️ External ping FAILED (server may not be publicly reachable yet).');
|
|
1363
|
-
console.log('Fix ingress (DNS/reverse proxy/firewall), then rerun
|
|
1378
|
+
console.log('Fix ingress (DNS/reverse proxy/firewall), then rerun. If you want to proceed anyway:');
|
|
1364
1379
|
console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
|
|
1380
|
+
console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --skip-verify`);
|
|
1365
1381
|
console.log('');
|
|
1366
1382
|
return;
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1371
1385
|
|
|
1372
1386
|
if (!config.getOnboarding().verify_confirmed) {
|
|
1373
1387
|
config.setOnboarding({
|
|
@@ -1496,46 +1510,73 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1496
1510
|
}
|
|
1497
1511
|
},
|
|
1498
1512
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1513
|
+
onboard: (args) => {
|
|
1514
|
+
const { A2AConfig } = require('../src/lib/config');
|
|
1515
|
+
const {
|
|
1516
|
+
readContextFiles,
|
|
1517
|
+
buildExtractionPrompt,
|
|
1518
|
+
validateDisclosureSubmission,
|
|
1519
|
+
saveManifest,
|
|
1520
|
+
MANIFEST_FILE
|
|
1521
|
+
} = require('../src/lib/disclosure');
|
|
1522
|
+
const config = new A2AConfig();
|
|
1508
1523
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1524
|
+
// ── Submit mode: agent sends structured JSON ──────────────
|
|
1525
|
+
const submitRaw = args.flags.submit;
|
|
1526
|
+
if (submitRaw) {
|
|
1527
|
+
let parsed;
|
|
1528
|
+
try {
|
|
1529
|
+
parsed = JSON.parse(String(submitRaw));
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
console.error('\n\u274c Invalid JSON in --submit flag.');
|
|
1532
|
+
console.error(` Parse error: ${e.message}\n`);
|
|
1533
|
+
process.exit(1);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const result = validateDisclosureSubmission(parsed);
|
|
1537
|
+
if (!result.valid) {
|
|
1538
|
+
console.error('\n\u274c Disclosure submission validation failed:\n');
|
|
1539
|
+
result.errors.forEach(err => console.error(` \u2022 ${err}`));
|
|
1540
|
+
console.error(`\nFix the errors above and resubmit with: a2a onboard --submit '<json>'\n`);
|
|
1541
|
+
process.exit(1);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
saveManifest(result.manifest);
|
|
1545
|
+
|
|
1546
|
+
const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || '';
|
|
1547
|
+
const hostname = args.flags.hostname || config.getAgent().hostname || process.env.A2A_HOSTNAME || '';
|
|
1548
|
+
if (agentName) config.setAgent({ name: agentName });
|
|
1549
|
+
if (hostname) config.setAgent({ hostname });
|
|
1512
1550
|
|
|
1551
|
+
console.log('\n\u2705 Disclosure manifest saved.');
|
|
1552
|
+
console.log(` Manifest: ${MANIFEST_FILE}`);
|
|
1553
|
+
console.log(' Next: a2a quickstart\n');
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// ── Prompt mode: print extraction instructions for agent ──
|
|
1558
|
+
if (config.isOnboarded() && !args.flags.force) {
|
|
1559
|
+
console.log('\u2705 Onboarding already complete. Use --force to regenerate.');
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
|
|
1513
1564
|
const contextFiles = readContextFiles(workspaceDir);
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || '';
|
|
1531
|
-
const hostname = args.flags.hostname || config.getAgent().hostname || process.env.A2A_HOSTNAME || '';
|
|
1532
|
-
if (agentName) config.setAgent({ name: agentName });
|
|
1533
|
-
if (hostname) config.setAgent({ hostname });
|
|
1534
|
-
|
|
1535
|
-
console.log(`\n\u2705 Disclosure manifest generated.`);
|
|
1536
|
-
console.log(` Manifest: ${MANIFEST_FILE}`);
|
|
1537
|
-
console.log(' Next: a2a quickstart\n');
|
|
1538
|
-
},
|
|
1565
|
+
|
|
1566
|
+
const availableFiles = {
|
|
1567
|
+
'USER.md': Boolean(contextFiles.user),
|
|
1568
|
+
'SOUL.md': Boolean(contextFiles.soul),
|
|
1569
|
+
'HEARTBEAT.md': Boolean(contextFiles.heartbeat),
|
|
1570
|
+
'SKILL.md': Boolean(contextFiles.skill),
|
|
1571
|
+
'CLAUDE.md': Boolean(contextFiles.claude),
|
|
1572
|
+
'memory/*.md': Boolean(contextFiles.memory)
|
|
1573
|
+
};
|
|
1574
|
+
|
|
1575
|
+
console.log(buildExtractionPrompt(availableFiles));
|
|
1576
|
+
console.log('\n---');
|
|
1577
|
+
console.log('After the owner confirms, submit with:');
|
|
1578
|
+
console.log(" a2a onboard --submit '<json>'\n");
|
|
1579
|
+
},
|
|
1539
1580
|
|
|
1540
1581
|
help: () => {
|
|
1541
1582
|
console.log(`A2A Calling - Agent-to-Agent Communication
|
package/package.json
CHANGED
|
@@ -153,7 +153,7 @@ function ensureConfigAndManifest(inviteHost, port, options = {}) {
|
|
|
153
153
|
|
|
154
154
|
try {
|
|
155
155
|
const { A2AConfig } = require('../src/lib/config');
|
|
156
|
-
const { loadManifest, saveManifest, generateDefaultManifest
|
|
156
|
+
const { loadManifest, saveManifest, generateDefaultManifest } = require('../src/lib/disclosure');
|
|
157
157
|
|
|
158
158
|
const config = new A2AConfig();
|
|
159
159
|
const defaults = config.getDefaults() || {};
|
|
@@ -167,8 +167,7 @@ function ensureConfigAndManifest(inviteHost, port, options = {}) {
|
|
|
167
167
|
|
|
168
168
|
const manifest = loadManifest();
|
|
169
169
|
if (!manifest || Object.keys(manifest).length === 0) {
|
|
170
|
-
const
|
|
171
|
-
const generated = generateDefaultManifest(contextFiles);
|
|
170
|
+
const generated = generateDefaultManifest();
|
|
172
171
|
saveManifest(generated);
|
|
173
172
|
const manifestFile = path.join(configDir, 'a2a-disclosure.json');
|
|
174
173
|
log(`Generated default disclosure manifest: ${manifestFile}`);
|
package/src/lib/disclosure.js
CHANGED
|
@@ -122,196 +122,170 @@ function formatTopicsForPrompt(tierTopics) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Generate a
|
|
126
|
-
*
|
|
125
|
+
* Generate a minimal starter manifest. This provides safe defaults when
|
|
126
|
+
* no agent-driven extraction has been performed yet.
|
|
127
|
+
*
|
|
128
|
+
* For proper topic extraction, use buildExtractionPrompt() to instruct
|
|
129
|
+
* an agent, then validate the result with validateDisclosureSubmission().
|
|
127
130
|
*/
|
|
128
|
-
function generateDefaultManifest(
|
|
131
|
+
function generateDefaultManifest() {
|
|
129
132
|
const now = new Date().toISOString();
|
|
130
133
|
|
|
131
|
-
|
|
134
|
+
return {
|
|
132
135
|
version: 1,
|
|
133
136
|
generated_at: now,
|
|
134
137
|
updated_at: now,
|
|
135
138
|
topics: {
|
|
136
|
-
public: {
|
|
139
|
+
public: {
|
|
140
|
+
lead_with: [{ topic: 'What I do', detail: 'Brief professional description' }],
|
|
141
|
+
discuss_freely: [{ topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }],
|
|
142
|
+
deflect: [{ topic: 'Personal details', detail: 'Redirect to direct owner contact' }]
|
|
143
|
+
},
|
|
137
144
|
friends: { lead_with: [], discuss_freely: [], deflect: [] },
|
|
138
145
|
family: { lead_with: [], discuss_freely: [], deflect: [] }
|
|
139
146
|
},
|
|
140
147
|
never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
|
|
141
148
|
personality_notes: 'Direct and technical. Prefers depth over breadth.'
|
|
142
149
|
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if a string contains technical content that shouldn't appear in
|
|
154
|
+
* disclosure topics (code snippets, URLs, markdown formatting, camelCase identifiers).
|
|
155
|
+
*/
|
|
156
|
+
function isTechnicalContent(line) {
|
|
157
|
+
return /`/.test(line) ||
|
|
158
|
+
/https?:\/\//.test(line) ||
|
|
159
|
+
/\*\*:/.test(line) ||
|
|
160
|
+
/:\*\*/.test(line) ||
|
|
161
|
+
/\b[a-z]{3,}[A-Z][a-z]{3,}/.test(line);
|
|
162
|
+
}
|
|
143
163
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
manifest
|
|
154
|
-
{ topic: 'What I do', detail: 'Brief professional description' }
|
|
155
|
-
);
|
|
156
|
-
manifest.topics.public.discuss_freely.push(
|
|
157
|
-
{ topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }
|
|
158
|
-
);
|
|
159
|
-
manifest.topics.public.deflect.push(
|
|
160
|
-
{ topic: 'Personal details', detail: 'Redirect to direct owner contact' }
|
|
161
|
-
);
|
|
162
|
-
return manifest;
|
|
164
|
+
/**
|
|
165
|
+
* Validate an agent-submitted disclosure submission against the expected schema.
|
|
166
|
+
* Returns { valid: boolean, manifest: object|null, errors: string[] }.
|
|
167
|
+
*/
|
|
168
|
+
function validateDisclosureSubmission(data) {
|
|
169
|
+
const errors = [];
|
|
170
|
+
|
|
171
|
+
// Must be a non-null object
|
|
172
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
173
|
+
return { valid: false, manifest: null, errors: ['Submission must be a non-null object'] };
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
//
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const goals = goalsMatch[1]
|
|
171
|
-
.split('\n')
|
|
172
|
-
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
173
|
-
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
174
|
-
.filter(Boolean);
|
|
175
|
-
|
|
176
|
-
goals.forEach((goal, i) => {
|
|
177
|
-
if (i < 2) {
|
|
178
|
-
manifest.topics.public.lead_with.push({ topic: goal.slice(0, 60), detail: goal });
|
|
179
|
-
} else {
|
|
180
|
-
manifest.topics.public.discuss_freely.push({ topic: goal.slice(0, 60), detail: goal });
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
}
|
|
176
|
+
// Require topics object
|
|
177
|
+
if (!data.topics || typeof data.topics !== 'object' || Array.isArray(data.topics)) {
|
|
178
|
+
errors.push('Submission must include a "topics" object');
|
|
179
|
+
return { valid: false, manifest: null, errors };
|
|
180
|
+
}
|
|
184
181
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
.split('\n')
|
|
190
|
-
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
191
|
-
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
192
|
-
.filter(Boolean);
|
|
193
|
-
|
|
194
|
-
interests.forEach(interest => {
|
|
195
|
-
manifest.topics.public.discuss_freely.push({ topic: interest.slice(0, 60), detail: interest });
|
|
196
|
-
});
|
|
182
|
+
// Require all three tiers
|
|
183
|
+
for (const tier of TIER_HIERARCHY) {
|
|
184
|
+
if (!data.topics[tier] || typeof data.topics[tier] !== 'object') {
|
|
185
|
+
errors.push(`Missing required tier: "${tier}" in topics`);
|
|
197
186
|
}
|
|
187
|
+
}
|
|
188
|
+
if (errors.length > 0) {
|
|
189
|
+
return { valid: false, manifest: null, errors };
|
|
190
|
+
}
|
|
198
191
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.split('\n')
|
|
204
|
-
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
205
|
-
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
206
|
-
.filter(Boolean);
|
|
207
|
-
|
|
208
|
-
privateItems.forEach(item => {
|
|
209
|
-
manifest.topics.family.discuss_freely.push({ topic: item.slice(0, 60), detail: item });
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// Deflect these for public
|
|
213
|
-
manifest.topics.public.deflect.push(
|
|
214
|
-
{ topic: 'Personal life', detail: 'Redirect — suggest owners connect directly' }
|
|
215
|
-
);
|
|
216
|
-
}
|
|
192
|
+
// Reject extra tiers beyond the known hierarchy
|
|
193
|
+
const extraTiers = Object.keys(data.topics).filter(t => !TIER_HIERARCHY.includes(t));
|
|
194
|
+
if (extraTiers.length > 0) {
|
|
195
|
+
errors.push(`Unknown tiers: ${extraTiers.join(', ')} — only public, friends, family are allowed`);
|
|
217
196
|
}
|
|
218
197
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
recentLines.forEach((line, i) => {
|
|
229
|
-
if (i < 2) {
|
|
230
|
-
manifest.topics.public.lead_with.push({ topic: line.slice(0, 60), detail: line });
|
|
231
|
-
} else {
|
|
232
|
-
manifest.topics.friends.discuss_freely.push({ topic: line.slice(0, 60), detail: line });
|
|
198
|
+
// Validate each tier's structure
|
|
199
|
+
const requiredLists = ['lead_with', 'discuss_freely', 'deflect'];
|
|
200
|
+
const LIST_LIMITS = { lead_with: 10, discuss_freely: 20, deflect: 10 };
|
|
201
|
+
for (const tier of TIER_HIERARCHY) {
|
|
202
|
+
const tierData = data.topics[tier];
|
|
203
|
+
for (const cat of requiredLists) {
|
|
204
|
+
if (!Array.isArray(tierData[cat])) {
|
|
205
|
+
errors.push(`topics.${tier}.${cat} must be an array`);
|
|
206
|
+
continue;
|
|
233
207
|
}
|
|
234
|
-
|
|
208
|
+
if (tierData[cat].length > LIST_LIMITS[cat]) {
|
|
209
|
+
errors.push(`topics.${tier}.${cat} has ${tierData[cat].length} items — max ${LIST_LIMITS[cat]}`);
|
|
210
|
+
}
|
|
211
|
+
for (let i = 0; i < tierData[cat].length; i++) {
|
|
212
|
+
const item = tierData[cat][i];
|
|
213
|
+
if (!item || typeof item !== 'object' || typeof item.topic !== 'string' || typeof item.detail !== 'string') {
|
|
214
|
+
errors.push(`topics.${tier}.${cat}[${i}]: each topic item must have "topic" (string) and "detail" (string)`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (item.topic.trim().length === 0) {
|
|
218
|
+
errors.push(`topics.${tier}.${cat}[${i}].topic must not be empty`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (item.topic.length > 160) {
|
|
222
|
+
errors.push(`topics.${tier}.${cat}[${i}]: topic exceeds 160 character limit (got ${item.topic.length})`);
|
|
223
|
+
}
|
|
224
|
+
if (item.detail.length > 500) {
|
|
225
|
+
errors.push(`topics.${tier}.${cat}[${i}]: detail exceeds 500 character limit (got ${item.detail.length})`);
|
|
226
|
+
}
|
|
227
|
+
if (isTechnicalContent(item.topic) || isTechnicalContent(item.detail)) {
|
|
228
|
+
errors.push(`topics.${tier}.${cat}[${i}]: contains technical content (code, URLs, or markdown formatting) — use plain language`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
235
232
|
}
|
|
236
233
|
|
|
237
|
-
//
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
234
|
+
// Validate never_disclose (optional, defaults to sensible list)
|
|
235
|
+
if (data.never_disclose !== undefined) {
|
|
236
|
+
if (!Array.isArray(data.never_disclose)) {
|
|
237
|
+
errors.push('"never_disclose" must be an array of strings');
|
|
238
|
+
} else {
|
|
239
|
+
if (data.never_disclose.length > 20) {
|
|
240
|
+
errors.push('never_disclose has too many items — max 20');
|
|
241
|
+
}
|
|
242
|
+
for (let i = 0; i < data.never_disclose.length; i++) {
|
|
243
|
+
if (typeof data.never_disclose[i] !== 'string') {
|
|
244
|
+
errors.push(`never_disclose[${i}] must be a string`);
|
|
245
|
+
} else if (data.never_disclose[i].length > 200) {
|
|
246
|
+
errors.push(`never_disclose[${i}] exceeds 200 chars`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
249
|
}
|
|
250
|
+
}
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
258
|
-
.filter(Boolean);
|
|
259
|
-
|
|
260
|
-
values.forEach(value => {
|
|
261
|
-
manifest.topics.friends.discuss_freely.push({ topic: value.slice(0, 60), detail: value });
|
|
262
|
-
});
|
|
252
|
+
// Validate personality_notes (optional)
|
|
253
|
+
if (data.personality_notes !== undefined) {
|
|
254
|
+
if (typeof data.personality_notes !== 'string') {
|
|
255
|
+
errors.push('"personality_notes" must be a string');
|
|
256
|
+
} else if (data.personality_notes.length > 500) {
|
|
257
|
+
errors.push('"personality_notes" exceeds 500 chars');
|
|
263
258
|
}
|
|
264
259
|
}
|
|
265
260
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const memoryLines = contextFiles.memory
|
|
269
|
-
.split('\n')
|
|
270
|
-
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
271
|
-
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
272
|
-
.filter(l => l.length > 5 && l.length < 120)
|
|
273
|
-
.slice(0, 4);
|
|
274
|
-
|
|
275
|
-
memoryLines.forEach(item => {
|
|
276
|
-
manifest.topics.friends.discuss_freely.push({ topic: item.slice(0, 60), detail: item });
|
|
277
|
-
});
|
|
261
|
+
if (errors.length > 0) {
|
|
262
|
+
return { valid: false, manifest: null, errors };
|
|
278
263
|
}
|
|
279
264
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
.slice(0, 3);
|
|
290
|
-
|
|
291
|
-
contextLines.forEach(item => {
|
|
292
|
-
manifest.topics.public.discuss_freely.push({ topic: item.slice(0, 60), detail: item });
|
|
293
|
-
});
|
|
265
|
+
// Rebuild topics from only validated keys to prevent extra properties passing through
|
|
266
|
+
const cleanTopics = {};
|
|
267
|
+
for (const tier of TIER_HIERARCHY) {
|
|
268
|
+
cleanTopics[tier] = {};
|
|
269
|
+
for (const cat of ['lead_with', 'discuss_freely', 'deflect']) {
|
|
270
|
+
cleanTopics[tier][cat] = (data.topics[tier][cat] || []).map(item => ({
|
|
271
|
+
topic: item.topic,
|
|
272
|
+
detail: item.detail
|
|
273
|
+
}));
|
|
294
274
|
}
|
|
295
275
|
}
|
|
296
276
|
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
if (manifest.topics.public.deflect.length === 0) {
|
|
309
|
-
manifest.topics.public.deflect.push(
|
|
310
|
-
{ topic: 'Private matters', detail: 'Redirect to direct owner contact' }
|
|
311
|
-
);
|
|
312
|
-
}
|
|
277
|
+
// Build valid manifest
|
|
278
|
+
const now = new Date().toISOString();
|
|
279
|
+
const manifest = {
|
|
280
|
+
version: 1,
|
|
281
|
+
generated_at: now,
|
|
282
|
+
updated_at: now,
|
|
283
|
+
topics: cleanTopics,
|
|
284
|
+
never_disclose: data.never_disclose || ['API keys', 'Other users\' data', 'Financial figures'],
|
|
285
|
+
personality_notes: data.personality_notes || ''
|
|
286
|
+
};
|
|
313
287
|
|
|
314
|
-
return manifest;
|
|
288
|
+
return { valid: true, manifest, errors: [] };
|
|
315
289
|
}
|
|
316
290
|
|
|
317
291
|
/**
|
|
@@ -355,6 +329,72 @@ function readContextFiles(workspaceDir) {
|
|
|
355
329
|
return result;
|
|
356
330
|
}
|
|
357
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Generate the extraction prompt that instructs an agent on exactly what
|
|
334
|
+
* structured disclosure data to return.
|
|
335
|
+
*
|
|
336
|
+
* @param {Object} [availableFiles] - Map of filename to truthy if present
|
|
337
|
+
* @returns {string} The instruction prompt for the agent
|
|
338
|
+
*/
|
|
339
|
+
function buildExtractionPrompt(availableFiles = {}) {
|
|
340
|
+
const fileList = Object.entries(availableFiles)
|
|
341
|
+
.filter(([, present]) => present)
|
|
342
|
+
.map(([name]) => ` - ${name}`)
|
|
343
|
+
.join('\n') || ' (no workspace files detected)';
|
|
344
|
+
|
|
345
|
+
const jsonBlock = '```json\n{\n "topics": {\n "public": {\n "lead_with": [\n { "topic": "Short label (max 160 chars)", "detail": "Longer description of the topic" }\n ],\n "discuss_freely": [],\n "deflect": []\n },\n "friends": {\n "lead_with": [],\n "discuss_freely": [],\n "deflect": []\n },\n "family": {\n "lead_with": [],\n "discuss_freely": [],\n "deflect": []\n }\n },\n "never_disclose": ["API keys", "Credentials", "Financial figures"],\n "personality_notes": "Brief description of communication style"\n}\n```';
|
|
346
|
+
|
|
347
|
+
return `## A2A Disclosure Extraction
|
|
348
|
+
|
|
349
|
+
You are helping the owner set up their A2A disclosure profile — the topics and information their agent is willing to discuss with other agents at different trust levels.
|
|
350
|
+
|
|
351
|
+
### Available workspace files
|
|
352
|
+
${fileList}
|
|
353
|
+
|
|
354
|
+
Read the available files above and extract disclosure topics. Focus on what the OWNER cares about, works on, and wants to discuss — NOT on agent instructions, code documentation, or operational tasks.
|
|
355
|
+
|
|
356
|
+
### What to extract
|
|
357
|
+
|
|
358
|
+
For each trust tier, identify topics the owner would want to discuss:
|
|
359
|
+
|
|
360
|
+
- **public** — safe for anyone: professional role, public interests, general project descriptions
|
|
361
|
+
- **friends** — for trusted contacts: current goals, collaboration interests, values, detailed project work
|
|
362
|
+
- **family** — inner circle only: personal interests, private projects, sensitive plans
|
|
363
|
+
|
|
364
|
+
For each tier, categorize topics as:
|
|
365
|
+
- **lead_with** — proactively bring up (max 3 per tier)
|
|
366
|
+
- **discuss_freely** — happy to discuss if asked (max 8 per tier)
|
|
367
|
+
- **deflect** — redirect or decline (max 3 per tier)
|
|
368
|
+
|
|
369
|
+
Also identify:
|
|
370
|
+
- **never_disclose** — information that should never be shared regardless of tier (API keys, credentials, financial data, etc.)
|
|
371
|
+
- **personality_notes** — a 1-2 sentence description of the owner's communication style
|
|
372
|
+
|
|
373
|
+
### What NOT to extract
|
|
374
|
+
|
|
375
|
+
Do NOT include as topics:
|
|
376
|
+
- Code snippets, CLI commands, or technical documentation
|
|
377
|
+
- URLs or file paths
|
|
378
|
+
- Agent instructions or operational tasks (e.g., "post 50 comments/day")
|
|
379
|
+
- Markdown formatting artifacts (bold markers, backticks)
|
|
380
|
+
- Anything from HEARTBEAT.md (these are agent tasks, not disclosure topics)
|
|
381
|
+
|
|
382
|
+
### Required JSON format
|
|
383
|
+
|
|
384
|
+
Return ONLY valid JSON in this exact structure:
|
|
385
|
+
|
|
386
|
+
${jsonBlock}
|
|
387
|
+
|
|
388
|
+
### Rules
|
|
389
|
+
|
|
390
|
+
1. Each "topic" string must be a short, human-readable label (max 160 chars)
|
|
391
|
+
2. Each "detail" string explains the topic more fully (max 500 chars)
|
|
392
|
+
3. Topics should be things a person would discuss, not technical artifacts
|
|
393
|
+
4. Higher tiers (friends, family) inherit lower-tier topics automatically — don't duplicate
|
|
394
|
+
5. Present this to the owner for review before submitting
|
|
395
|
+
6. The owner may edit, remove, or add topics before final submission`;
|
|
396
|
+
}
|
|
397
|
+
|
|
358
398
|
module.exports = {
|
|
359
399
|
loadManifest,
|
|
360
400
|
saveManifest,
|
|
@@ -362,5 +402,8 @@ module.exports = {
|
|
|
362
402
|
formatTopicsForPrompt,
|
|
363
403
|
generateDefaultManifest,
|
|
364
404
|
readContextFiles,
|
|
405
|
+
validateDisclosureSubmission,
|
|
406
|
+
isTechnicalContent,
|
|
407
|
+
buildExtractionPrompt,
|
|
365
408
|
MANIFEST_FILE
|
|
366
409
|
};
|