a2acalling 0.6.4 → 0.6.6
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 +13 -0
- package/SKILL.md +29 -1
- package/bin/cli.js +890 -474
- package/docs/plans/2026-02-14-agent-driven-disclosure-extraction.md +986 -0
- package/package.json +2 -1
- package/scripts/install-openclaw.js +2 -3
- package/scripts/postinstall.js +25 -0
- package/src/lib/disclosure.js +362 -155
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "a2acalling",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"a2acalling": "scripts/install-openclaw.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
+
"postinstall": "node scripts/postinstall.js",
|
|
11
12
|
"start": "node src/server.js",
|
|
12
13
|
"test": "node test/run.js"
|
|
13
14
|
},
|
|
@@ -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}`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Only show the banner for global installs; skip in CI, dev, and Docker builds.
|
|
4
|
+
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) process.exit(0);
|
|
5
|
+
if (process.env.npm_config_global !== 'true') process.exit(0);
|
|
6
|
+
|
|
7
|
+
console.log(`
|
|
8
|
+
╔══════════════════════════════════════════════════════════╗
|
|
9
|
+
║ 🤝 A2A Calling installed successfully! ║
|
|
10
|
+
╠══════════════════════════════════════════════════════════╣
|
|
11
|
+
║ ║
|
|
12
|
+
║ Next step: ║
|
|
13
|
+
║ a2a quickstart --hostname YOUR_DOMAIN:PORT ║
|
|
14
|
+
║ ║
|
|
15
|
+
║ Example: ║
|
|
16
|
+
║ a2a quickstart --hostname myserver.com:3001 ║
|
|
17
|
+
║ ║
|
|
18
|
+
║ This will: ║
|
|
19
|
+
║ • Configure your agent's disclosure topics ║
|
|
20
|
+
║ • Set up permission tiers (public/friends/family) ║
|
|
21
|
+
║ • Start the A2A server ║
|
|
22
|
+
║ • Generate your first invite to share ║
|
|
23
|
+
║ ║
|
|
24
|
+
╚══════════════════════════════════════════════════════════╝
|
|
25
|
+
`);
|
package/src/lib/disclosure.js
CHANGED
|
@@ -17,6 +17,62 @@ const MANIFEST_FILE = path.join(CONFIG_DIR, 'a2a-disclosure.json');
|
|
|
17
17
|
|
|
18
18
|
const TIER_HIERARCHY = ['public', 'friends', 'family'];
|
|
19
19
|
const logger = createLogger({ component: 'a2a.disclosure' });
|
|
20
|
+
const SKIP_FILES = new Set(['heartbeat', 'skill', 'claude']);
|
|
21
|
+
|
|
22
|
+
function normalizeTopic(raw) {
|
|
23
|
+
return String(raw || '').trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dedupeByTopic(items) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
const topic = normalizeTopic(item && item.topic);
|
|
31
|
+
if (!topic || seen.has(topic.toLowerCase())) continue;
|
|
32
|
+
seen.add(topic.toLowerCase());
|
|
33
|
+
out.push({
|
|
34
|
+
topic,
|
|
35
|
+
detail: normalizeTopic(item && item.detail)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseTopicLine(rawLine) {
|
|
42
|
+
const line = normalizeTopic(rawLine);
|
|
43
|
+
if (!line) return null;
|
|
44
|
+
|
|
45
|
+
const splitPoint = line.search(/\s+[-–—:]\s+/);
|
|
46
|
+
if (splitPoint > 10) {
|
|
47
|
+
const topic = normalizeTopic(line.slice(0, splitPoint));
|
|
48
|
+
const detail = normalizeTopic(line.slice(splitPoint + 3));
|
|
49
|
+
return { topic, detail };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { topic: line, detail: '' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isValidTopic(line) {
|
|
56
|
+
if (!line || line.length < 5) return false;
|
|
57
|
+
if (line.includes('`')) return false;
|
|
58
|
+
if (line.includes('http')) return false;
|
|
59
|
+
if (line.includes('**:')) return false;
|
|
60
|
+
if (line.startsWith('//')) return false;
|
|
61
|
+
if (line.includes('()')) return false;
|
|
62
|
+
if (/\d{4}-\d{2}-\d{2}/.test(line)) return false;
|
|
63
|
+
if (line.toLowerCase().includes('todo')) return false;
|
|
64
|
+
if (line.toLowerCase().includes('fixme')) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function truncateAtWordBoundary(text, max = 60) {
|
|
69
|
+
const normalized = normalizeTopic(text);
|
|
70
|
+
if (normalized.length <= max) return normalized;
|
|
71
|
+
|
|
72
|
+
const truncated = normalized.slice(0, max);
|
|
73
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
74
|
+
return (lastSpace > 20 ? truncated.slice(0, lastSpace) : truncated) + '...';
|
|
75
|
+
}
|
|
20
76
|
|
|
21
77
|
/**
|
|
22
78
|
* Load manifest from disk. Returns {} if not found.
|
|
@@ -122,196 +178,278 @@ function formatTopicsForPrompt(tierTopics) {
|
|
|
122
178
|
}
|
|
123
179
|
|
|
124
180
|
/**
|
|
125
|
-
* Generate a
|
|
126
|
-
*
|
|
181
|
+
* Generate a minimal starter manifest. This provides safe defaults when
|
|
182
|
+
* no agent-driven extraction has been performed yet.
|
|
183
|
+
*
|
|
184
|
+
* For proper topic extraction, use buildExtractionPrompt() to instruct
|
|
185
|
+
* an agent, then validate the result with validateDisclosureSubmission().
|
|
127
186
|
*/
|
|
128
187
|
function generateDefaultManifest(contextFiles = {}) {
|
|
129
188
|
const now = new Date().toISOString();
|
|
189
|
+
const source = {};
|
|
190
|
+
const raw = contextFiles || {};
|
|
191
|
+
Object.keys(raw).forEach((key) => {
|
|
192
|
+
if (!SKIP_FILES.has(key.toLowerCase())) {
|
|
193
|
+
source[key] = raw[key];
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const userContent = String(source.user || '');
|
|
198
|
+
const soulContent = String(source.soul || '');
|
|
199
|
+
function extractFromSource(content, sectionNames) {
|
|
200
|
+
const sectionPattern = new RegExp(
|
|
201
|
+
`##\\s*(?:${sectionNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})[^\\n]*\\n([\\s\\S]*?)(?=\\n##|$)`,
|
|
202
|
+
'i'
|
|
203
|
+
);
|
|
204
|
+
const match = String(content || '').match(sectionPattern);
|
|
205
|
+
if (!match) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
130
208
|
|
|
131
|
-
|
|
209
|
+
return String(match[1] || '')
|
|
210
|
+
.split('\n')
|
|
211
|
+
.map(line => normalizeTopic(line))
|
|
212
|
+
.filter(line => line.startsWith('-') || line.startsWith('*'))
|
|
213
|
+
.map(line => normalizeTopic(line.replace(/^[\s\-\*]+/, '')))
|
|
214
|
+
.map(parseTopicLine)
|
|
215
|
+
.filter(topic => topic && isValidTopic(topic.topic))
|
|
216
|
+
.map(topic => ({
|
|
217
|
+
topic: truncateAtWordBoundary(topic.topic, 60),
|
|
218
|
+
detail: truncateAtWordBoundary(topic.detail || '', 120)
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const candidateTopics = dedupeByTopic([
|
|
223
|
+
...extractFromSource(userContent, ['Goals', 'Interests', 'Projects', 'Current']),
|
|
224
|
+
...extractFromSource(soulContent, ['Goals', 'Interests', 'Projects', 'Current', 'Values', 'Personal'])
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
if (candidateTopics.length === 0) {
|
|
228
|
+
return {
|
|
229
|
+
version: 1,
|
|
230
|
+
generated_at: now,
|
|
231
|
+
updated_at: now,
|
|
232
|
+
topics: {
|
|
233
|
+
public: {
|
|
234
|
+
lead_with: [{ topic: 'What I do', detail: 'Brief professional description' }],
|
|
235
|
+
discuss_freely: [{ topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }],
|
|
236
|
+
deflect: [{ topic: 'Personal details', detail: 'Redirect to direct owner contact' }]
|
|
237
|
+
},
|
|
238
|
+
friends: { lead_with: [], discuss_freely: [], deflect: [] },
|
|
239
|
+
family: { lead_with: [], discuss_freely: [], deflect: [] }
|
|
240
|
+
},
|
|
241
|
+
never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
|
|
242
|
+
personality_notes: 'Direct and technical. Prefers depth over breadth.'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const publicLead = [];
|
|
247
|
+
const publicDiscuss = [];
|
|
248
|
+
const publicDeflect = [];
|
|
249
|
+
const friendsLead = [];
|
|
250
|
+
const friendsDiscuss = [];
|
|
251
|
+
const familyDiscuss = [];
|
|
252
|
+
|
|
253
|
+
candidateTopics.forEach((entry, index) => {
|
|
254
|
+
const topic = truncateAtWordBoundary(entry.topic || '', 60);
|
|
255
|
+
const detail = truncateAtWordBoundary(entry.detail || 'Open discussion topic.', 120);
|
|
256
|
+
if (!topic) return;
|
|
257
|
+
|
|
258
|
+
const node = { topic, detail };
|
|
259
|
+
if (index < 2) {
|
|
260
|
+
publicLead.push(node);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (index < 6) {
|
|
264
|
+
publicDiscuss.push(node);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (index < 8) {
|
|
268
|
+
friendsLead.push(node);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (index < 12) {
|
|
272
|
+
friendsDiscuss.push(node);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (index < 14) {
|
|
276
|
+
familyDiscuss.push(node);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
if (publicLead.length === 0) {
|
|
281
|
+
publicLead.push({ topic: 'Open source', detail: 'General product and engineering topics.' });
|
|
282
|
+
}
|
|
283
|
+
if (publicDiscuss.length === 0) {
|
|
284
|
+
publicDiscuss.push({ topic: 'Collaboration', detail: 'Ways to collaborate and support each other.' });
|
|
285
|
+
}
|
|
286
|
+
if (publicDeflect.length === 0) {
|
|
287
|
+
publicDeflect.push({ topic: 'Personal details', detail: 'Redirect to direct owner contact.' });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
132
291
|
version: 1,
|
|
133
292
|
generated_at: now,
|
|
134
293
|
updated_at: now,
|
|
135
294
|
topics: {
|
|
136
|
-
public: {
|
|
137
|
-
|
|
138
|
-
|
|
295
|
+
public: {
|
|
296
|
+
lead_with: publicLead,
|
|
297
|
+
discuss_freely: publicDiscuss,
|
|
298
|
+
deflect: publicDeflect
|
|
299
|
+
},
|
|
300
|
+
friends: {
|
|
301
|
+
lead_with: friendsLead,
|
|
302
|
+
discuss_freely: friendsDiscuss,
|
|
303
|
+
deflect: []
|
|
304
|
+
},
|
|
305
|
+
family: {
|
|
306
|
+
lead_with: [],
|
|
307
|
+
discuss_freely: familyDiscuss,
|
|
308
|
+
deflect: []
|
|
309
|
+
}
|
|
139
310
|
},
|
|
140
311
|
never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
|
|
141
|
-
personality_notes: 'Direct and
|
|
312
|
+
personality_notes: 'Direct and practical. Open to collaboration with clear boundaries.'
|
|
142
313
|
};
|
|
314
|
+
}
|
|
143
315
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
316
|
+
/**
|
|
317
|
+
* Check if a string contains technical content that shouldn't appear in
|
|
318
|
+
* disclosure topics (code snippets, URLs, markdown formatting, camelCase identifiers).
|
|
319
|
+
*/
|
|
320
|
+
function isTechnicalContent(line) {
|
|
321
|
+
return /`/.test(line) ||
|
|
322
|
+
/https?:\/\//.test(line) ||
|
|
323
|
+
/\*\*:/.test(line) ||
|
|
324
|
+
/:\*\*/.test(line) ||
|
|
325
|
+
/\b[a-z]{3,}[A-Z][a-z]{3,}/.test(line);
|
|
326
|
+
}
|
|
147
327
|
|
|
148
|
-
|
|
149
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Validate an agent-submitted disclosure submission against the expected schema.
|
|
330
|
+
* Returns { valid: boolean, manifest: object|null, errors: string[] }.
|
|
331
|
+
*/
|
|
332
|
+
function validateDisclosureSubmission(data) {
|
|
333
|
+
const errors = [];
|
|
150
334
|
|
|
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;
|
|
335
|
+
// Must be a non-null object
|
|
336
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
337
|
+
return { valid: false, manifest: null, errors: ['Submission must be a non-null object'] };
|
|
163
338
|
}
|
|
164
339
|
|
|
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
|
-
}
|
|
340
|
+
// Require topics object
|
|
341
|
+
if (!data.topics || typeof data.topics !== 'object' || Array.isArray(data.topics)) {
|
|
342
|
+
errors.push('Submission must include a "topics" object');
|
|
343
|
+
return { valid: false, manifest: null, errors };
|
|
344
|
+
}
|
|
184
345
|
|
|
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
|
-
});
|
|
346
|
+
// Require all three tiers
|
|
347
|
+
for (const tier of TIER_HIERARCHY) {
|
|
348
|
+
if (!data.topics[tier] || typeof data.topics[tier] !== 'object') {
|
|
349
|
+
errors.push(`Missing required tier: "${tier}" in topics`);
|
|
197
350
|
}
|
|
351
|
+
}
|
|
352
|
+
if (errors.length > 0) {
|
|
353
|
+
return { valid: false, manifest: null, errors };
|
|
354
|
+
}
|
|
198
355
|
|
|
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
|
-
}
|
|
356
|
+
// Reject extra tiers beyond the known hierarchy
|
|
357
|
+
const extraTiers = Object.keys(data.topics).filter(t => !TIER_HIERARCHY.includes(t));
|
|
358
|
+
if (extraTiers.length > 0) {
|
|
359
|
+
errors.push(`Unknown tiers: ${extraTiers.join(', ')} — only public, friends, family are allowed`);
|
|
217
360
|
}
|
|
218
361
|
|
|
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 });
|
|
362
|
+
// Validate each tier's structure
|
|
363
|
+
const requiredLists = ['lead_with', 'discuss_freely', 'deflect'];
|
|
364
|
+
const LIST_LIMITS = { lead_with: 10, discuss_freely: 20, deflect: 10 };
|
|
365
|
+
for (const tier of TIER_HIERARCHY) {
|
|
366
|
+
const tierData = data.topics[tier];
|
|
367
|
+
for (const cat of requiredLists) {
|
|
368
|
+
if (!Array.isArray(tierData[cat])) {
|
|
369
|
+
errors.push(`topics.${tier}.${cat} must be an array`);
|
|
370
|
+
continue;
|
|
233
371
|
}
|
|
234
|
-
|
|
372
|
+
if (tierData[cat].length > LIST_LIMITS[cat]) {
|
|
373
|
+
errors.push(`topics.${tier}.${cat} has ${tierData[cat].length} items — max ${LIST_LIMITS[cat]}`);
|
|
374
|
+
}
|
|
375
|
+
for (let i = 0; i < tierData[cat].length; i++) {
|
|
376
|
+
const item = tierData[cat][i];
|
|
377
|
+
if (!item || typeof item !== 'object' || typeof item.topic !== 'string' || typeof item.detail !== 'string') {
|
|
378
|
+
errors.push(`topics.${tier}.${cat}[${i}]: each topic item must have "topic" (string) and "detail" (string)`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (item.topic.trim().length === 0) {
|
|
382
|
+
errors.push(`topics.${tier}.${cat}[${i}].topic must not be empty`);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (item.topic.length > 160) {
|
|
386
|
+
errors.push(`topics.${tier}.${cat}[${i}]: topic exceeds 160 character limit (got ${item.topic.length})`);
|
|
387
|
+
}
|
|
388
|
+
if (item.detail.length > 500) {
|
|
389
|
+
errors.push(`topics.${tier}.${cat}[${i}]: detail exceeds 500 character limit (got ${item.detail.length})`);
|
|
390
|
+
}
|
|
391
|
+
if (isTechnicalContent(item.topic) || isTechnicalContent(item.detail)) {
|
|
392
|
+
errors.push(`topics.${tier}.${cat}[${i}]: contains technical content (code, URLs, or markdown formatting) — use plain language`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
235
396
|
}
|
|
236
397
|
|
|
237
|
-
//
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
398
|
+
// Validate never_disclose (optional, defaults to sensible list)
|
|
399
|
+
if (data.never_disclose !== undefined) {
|
|
400
|
+
if (!Array.isArray(data.never_disclose)) {
|
|
401
|
+
errors.push('"never_disclose" must be an array of strings');
|
|
402
|
+
} else {
|
|
403
|
+
if (data.never_disclose.length > 20) {
|
|
404
|
+
errors.push('never_disclose has too many items — max 20');
|
|
405
|
+
}
|
|
406
|
+
for (let i = 0; i < data.never_disclose.length; i++) {
|
|
407
|
+
if (typeof data.never_disclose[i] !== 'string') {
|
|
408
|
+
errors.push(`never_disclose[${i}] must be a string`);
|
|
409
|
+
} else if (data.never_disclose[i].length > 200) {
|
|
410
|
+
errors.push(`never_disclose[${i}] exceeds 200 chars`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
249
413
|
}
|
|
414
|
+
}
|
|
250
415
|
|
|
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
|
-
});
|
|
416
|
+
// Validate personality_notes (optional)
|
|
417
|
+
if (data.personality_notes !== undefined) {
|
|
418
|
+
if (typeof data.personality_notes !== 'string') {
|
|
419
|
+
errors.push('"personality_notes" must be a string');
|
|
420
|
+
} else if (data.personality_notes.length > 500) {
|
|
421
|
+
errors.push('"personality_notes" exceeds 500 chars');
|
|
263
422
|
}
|
|
264
423
|
}
|
|
265
424
|
|
|
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
|
-
});
|
|
425
|
+
if (errors.length > 0) {
|
|
426
|
+
return { valid: false, manifest: null, errors };
|
|
278
427
|
}
|
|
279
428
|
|
|
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
|
-
});
|
|
429
|
+
// Rebuild topics from only validated keys to prevent extra properties passing through
|
|
430
|
+
const cleanTopics = {};
|
|
431
|
+
for (const tier of TIER_HIERARCHY) {
|
|
432
|
+
cleanTopics[tier] = {};
|
|
433
|
+
for (const cat of ['lead_with', 'discuss_freely', 'deflect']) {
|
|
434
|
+
cleanTopics[tier][cat] = (data.topics[tier][cat] || []).map(item => ({
|
|
435
|
+
topic: item.topic,
|
|
436
|
+
detail: item.detail
|
|
437
|
+
}));
|
|
294
438
|
}
|
|
295
439
|
}
|
|
296
440
|
|
|
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
|
-
}
|
|
441
|
+
// Build valid manifest
|
|
442
|
+
const now = new Date().toISOString();
|
|
443
|
+
const manifest = {
|
|
444
|
+
version: 1,
|
|
445
|
+
generated_at: now,
|
|
446
|
+
updated_at: now,
|
|
447
|
+
topics: cleanTopics,
|
|
448
|
+
never_disclose: data.never_disclose || ['API keys', 'Other users\' data', 'Financial figures'],
|
|
449
|
+
personality_notes: data.personality_notes || ''
|
|
450
|
+
};
|
|
313
451
|
|
|
314
|
-
return manifest;
|
|
452
|
+
return { valid: true, manifest, errors: [] };
|
|
315
453
|
}
|
|
316
454
|
|
|
317
455
|
/**
|
|
@@ -355,6 +493,72 @@ function readContextFiles(workspaceDir) {
|
|
|
355
493
|
return result;
|
|
356
494
|
}
|
|
357
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Generate the extraction prompt that instructs an agent on exactly what
|
|
498
|
+
* structured disclosure data to return.
|
|
499
|
+
*
|
|
500
|
+
* @param {Object} [availableFiles] - Map of filename to truthy if present
|
|
501
|
+
* @returns {string} The instruction prompt for the agent
|
|
502
|
+
*/
|
|
503
|
+
function buildExtractionPrompt(availableFiles = {}) {
|
|
504
|
+
const fileList = Object.entries(availableFiles)
|
|
505
|
+
.filter(([, present]) => present)
|
|
506
|
+
.map(([name]) => ` - ${name}`)
|
|
507
|
+
.join('\n') || ' (no workspace files detected)';
|
|
508
|
+
|
|
509
|
+
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```';
|
|
510
|
+
|
|
511
|
+
return `## A2A Disclosure Extraction
|
|
512
|
+
|
|
513
|
+
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.
|
|
514
|
+
|
|
515
|
+
### Available workspace files
|
|
516
|
+
${fileList}
|
|
517
|
+
|
|
518
|
+
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.
|
|
519
|
+
|
|
520
|
+
### What to extract
|
|
521
|
+
|
|
522
|
+
For each trust tier, identify topics the owner would want to discuss:
|
|
523
|
+
|
|
524
|
+
- **public** — safe for anyone: professional role, public interests, general project descriptions
|
|
525
|
+
- **friends** — for trusted contacts: current goals, collaboration interests, values, detailed project work
|
|
526
|
+
- **family** — inner circle only: personal interests, private projects, sensitive plans
|
|
527
|
+
|
|
528
|
+
For each tier, categorize topics as:
|
|
529
|
+
- **lead_with** — proactively bring up (max 3 per tier)
|
|
530
|
+
- **discuss_freely** — happy to discuss if asked (max 8 per tier)
|
|
531
|
+
- **deflect** — redirect or decline (max 3 per tier)
|
|
532
|
+
|
|
533
|
+
Also identify:
|
|
534
|
+
- **never_disclose** — information that should never be shared regardless of tier (API keys, credentials, financial data, etc.)
|
|
535
|
+
- **personality_notes** — a 1-2 sentence description of the owner's communication style
|
|
536
|
+
|
|
537
|
+
### What NOT to extract
|
|
538
|
+
|
|
539
|
+
Do NOT include as topics:
|
|
540
|
+
- Code snippets, CLI commands, or technical documentation
|
|
541
|
+
- URLs or file paths
|
|
542
|
+
- Agent instructions or operational tasks (e.g., "post 50 comments/day")
|
|
543
|
+
- Markdown formatting artifacts (bold markers, backticks)
|
|
544
|
+
- Anything from HEARTBEAT.md (these are agent tasks, not disclosure topics)
|
|
545
|
+
|
|
546
|
+
### Required JSON format
|
|
547
|
+
|
|
548
|
+
Return ONLY valid JSON in this exact structure:
|
|
549
|
+
|
|
550
|
+
${jsonBlock}
|
|
551
|
+
|
|
552
|
+
### Rules
|
|
553
|
+
|
|
554
|
+
1. Each "topic" string must be a short, human-readable label (max 160 chars)
|
|
555
|
+
2. Each "detail" string explains the topic more fully (max 500 chars)
|
|
556
|
+
3. Topics should be things a person would discuss, not technical artifacts
|
|
557
|
+
4. Higher tiers (friends, family) inherit lower-tier topics automatically — don't duplicate
|
|
558
|
+
5. Present this to the owner for review before submitting
|
|
559
|
+
6. The owner may edit, remove, or add topics before final submission`;
|
|
560
|
+
}
|
|
561
|
+
|
|
358
562
|
module.exports = {
|
|
359
563
|
loadManifest,
|
|
360
564
|
saveManifest,
|
|
@@ -362,5 +566,8 @@ module.exports = {
|
|
|
362
566
|
formatTopicsForPrompt,
|
|
363
567
|
generateDefaultManifest,
|
|
364
568
|
readContextFiles,
|
|
569
|
+
validateDisclosureSubmission,
|
|
570
|
+
isTechnicalContent,
|
|
571
|
+
buildExtractionPrompt,
|
|
365
572
|
MANIFEST_FILE
|
|
366
573
|
};
|