a2acalling 0.6.51 → 0.6.53
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 +1 -0
- package/bin/cli.js +83 -7
- package/docs/protocol.md +6 -5
- package/native/macos/index.html +24 -12
- package/native/macos/package-lock.json +232 -0
- package/native/macos/src-tauri/src/discovery.rs +100 -13
- package/native/macos/src-tauri/src/health.rs +2 -3
- package/native/macos/src-tauri/src/notifications.rs +1 -1
- package/native/macos/src-tauri/src/server.rs +4 -1
- package/package.json +1 -1
- package/src/dashboard/public/app.js +2 -0
- package/src/dashboard/public/index.html +1 -0
- package/src/lib/claude-subagent.js +302 -92
- package/src/lib/config.js +11 -0
- package/src/lib/conversation-driver.js +18 -2
- package/src/lib/disclosure.js +89 -13
- package/src/lib/runtime-adapter.js +76 -20
- package/src/lib/tokens.js +18 -0
- package/src/routes/a2a.js +7 -0
- package/src/routes/dashboard.js +9 -1
- package/src/server.js +44 -2
|
@@ -128,6 +128,12 @@ class ConversationDriver {
|
|
|
128
128
|
this.maxTurns = options.maxTurns || 30;
|
|
129
129
|
this.onTurn = options.onTurn || null;
|
|
130
130
|
this.tier = options.tier || 'public';
|
|
131
|
+
// Optional permission envelope for runtimes (primarily Claude mode).
|
|
132
|
+
// If provided by caller, this keeps tool allowlists variable per token/profile.
|
|
133
|
+
this.capabilities = Array.isArray(options.capabilities) ? options.capabilities : [];
|
|
134
|
+
this.allowedTopics = Array.isArray(options.allowedTopics) ? options.allowedTopics : [];
|
|
135
|
+
this.allowedGoals = Array.isArray(options.allowedGoals) ? options.allowedGoals : [];
|
|
136
|
+
this.allowedTools = Array.isArray(options.allowedTools) ? options.allowedTools : [];
|
|
131
137
|
this.summarizer = options.summarizer || null;
|
|
132
138
|
this.ownerContext = options.ownerContext || {};
|
|
133
139
|
this.claudeMode = options.runtime?.mode === 'claude';
|
|
@@ -225,8 +231,11 @@ class ConversationDriver {
|
|
|
225
231
|
// Try runtime.summarize if available (OpenClaw path)
|
|
226
232
|
if (typeof runtime.summarize === 'function') {
|
|
227
233
|
try {
|
|
234
|
+
const summarySessionId = this.lastConversationId
|
|
235
|
+
? `a2a-${this.lastConversationId}`
|
|
236
|
+
: `summary-${Date.now()}`;
|
|
228
237
|
return await runtime.summarize({
|
|
229
|
-
sessionId:
|
|
238
|
+
sessionId: summarySessionId,
|
|
230
239
|
prompt,
|
|
231
240
|
messages,
|
|
232
241
|
callerInfo: { name: agentContext.name, owner: agentContext.owner },
|
|
@@ -407,7 +416,14 @@ class ConversationDriver {
|
|
|
407
416
|
tier: this.tier,
|
|
408
417
|
ownerName: this.agentContext.owner,
|
|
409
418
|
agentName: this.agentContext.name,
|
|
410
|
-
roleContext: 'You initiated this call.'
|
|
419
|
+
roleContext: 'You initiated this call.',
|
|
420
|
+
capabilities: this.capabilities,
|
|
421
|
+
allowedTopics: this.allowedTopics,
|
|
422
|
+
allowed_topics: this.allowedTopics,
|
|
423
|
+
allowedGoals: this.allowedGoals,
|
|
424
|
+
allowed_goals: this.allowedGoals,
|
|
425
|
+
allowedTools: this.allowedTools,
|
|
426
|
+
allowed_tools: this.allowedTools
|
|
411
427
|
};
|
|
412
428
|
if (this.claudeMode) {
|
|
413
429
|
contextPayload.turnCount = turn + 1;
|
package/src/lib/disclosure.js
CHANGED
|
@@ -18,6 +18,17 @@ const MANIFEST_FILE = path.join(CONFIG_DIR, 'a2a-disclosure.json');
|
|
|
18
18
|
const TIER_HIERARCHY = ['public', 'friends', 'family'];
|
|
19
19
|
const logger = createLogger({ component: 'a2a.disclosure' });
|
|
20
20
|
const SKIP_FILES = new Set(['heartbeat', 'skill', 'claude']);
|
|
21
|
+
const CANONICAL_TOOL_NAMES = ['Bash', 'Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
|
|
22
|
+
const TOOL_NAME_MAP = {
|
|
23
|
+
bash: 'Bash',
|
|
24
|
+
'bash(readonly)': 'Bash(readonly)',
|
|
25
|
+
'bash-readonly': 'Bash(readonly)',
|
|
26
|
+
read: 'Read',
|
|
27
|
+
grep: 'Grep',
|
|
28
|
+
glob: 'Glob',
|
|
29
|
+
websearch: 'WebSearch',
|
|
30
|
+
webfetch: 'WebFetch'
|
|
31
|
+
};
|
|
21
32
|
|
|
22
33
|
function normalizeTopic(raw) {
|
|
23
34
|
return String(raw || '').trim();
|
|
@@ -68,6 +79,26 @@ function dedupeDoNotDiscuss(items) {
|
|
|
68
79
|
return out;
|
|
69
80
|
}
|
|
70
81
|
|
|
82
|
+
function dedupeStringList(items, maxLength = 80) {
|
|
83
|
+
if (!Array.isArray(items)) return [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const item of items) {
|
|
87
|
+
const normalized = normalizeTopic(item).slice(0, maxLength);
|
|
88
|
+
if (!normalized) continue;
|
|
89
|
+
const key = normalized.toLowerCase();
|
|
90
|
+
if (seen.has(key)) continue;
|
|
91
|
+
seen.add(key);
|
|
92
|
+
out.push(normalized);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeToolName(value) {
|
|
98
|
+
const key = normalizeTopic(value).toLowerCase();
|
|
99
|
+
return TOOL_NAME_MAP[key] || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
function parseTopicLine(rawLine) {
|
|
72
103
|
const line = normalizeTopic(rawLine);
|
|
73
104
|
if (!line) return null;
|
|
@@ -148,7 +179,7 @@ function saveManifest(manifest) {
|
|
|
148
179
|
* Get topics for a given tier, merged down the hierarchy.
|
|
149
180
|
* family gets everything, friends gets friends+public, public gets public only.
|
|
150
181
|
*
|
|
151
|
-
* Returns { topics, objectives, do_not_discuss, never_disclose }
|
|
182
|
+
* Returns { topics, objectives, do_not_discuss, never_disclose, allowed_tools }
|
|
152
183
|
*/
|
|
153
184
|
function getTopicsForTier(tier) {
|
|
154
185
|
const manifest = loadManifest();
|
|
@@ -167,7 +198,8 @@ function getTopicsForTier(tier) {
|
|
|
167
198
|
topics: [],
|
|
168
199
|
objectives: [],
|
|
169
200
|
do_not_discuss: [],
|
|
170
|
-
never_disclose: manifest.never_disclose || []
|
|
201
|
+
never_disclose: manifest.never_disclose || [],
|
|
202
|
+
allowed_tools: []
|
|
171
203
|
};
|
|
172
204
|
|
|
173
205
|
for (const t of tiersToMerge) {
|
|
@@ -175,6 +207,7 @@ function getTopicsForTier(tier) {
|
|
|
175
207
|
if (tierData.topics) merged.topics.push(...tierData.topics);
|
|
176
208
|
if (tierData.objectives) merged.objectives.push(...tierData.objectives);
|
|
177
209
|
if (tierData.do_not_discuss) merged.do_not_discuss.push(...tierData.do_not_discuss);
|
|
210
|
+
if (tierData.allowed_tools) merged.allowed_tools.push(...tierData.allowed_tools);
|
|
178
211
|
}
|
|
179
212
|
|
|
180
213
|
// Remove do_not_discuss items that appear in topics (higher tiers promote them)
|
|
@@ -185,6 +218,7 @@ function getTopicsForTier(tier) {
|
|
|
185
218
|
merged.topics = dedupeByTopic(merged.topics);
|
|
186
219
|
merged.objectives = dedupeByObjective(merged.objectives);
|
|
187
220
|
merged.do_not_discuss = dedupeDoNotDiscuss(merged.do_not_discuss);
|
|
221
|
+
merged.allowed_tools = dedupeStringList(merged.allowed_tools, 80);
|
|
188
222
|
|
|
189
223
|
return merged;
|
|
190
224
|
}
|
|
@@ -212,6 +246,9 @@ function formatTopicsForPrompt(tierTopics) {
|
|
|
212
246
|
topics: formatTopicList(tierTopics.topics),
|
|
213
247
|
objectives: formatObjectiveList(tierTopics.objectives),
|
|
214
248
|
doNotDiscuss: formatDoNotDiscuss(tierTopics.do_not_discuss),
|
|
249
|
+
allowedTools: tierTopics.allowed_tools?.length
|
|
250
|
+
? tierTopics.allowed_tools.map(item => ` - ${item}`).join('\n')
|
|
251
|
+
: ' (none specified)',
|
|
215
252
|
neverDisclose: tierTopics.never_disclose?.length
|
|
216
253
|
? tierTopics.never_disclose.map(item => ` - ${item}`).join('\n')
|
|
217
254
|
: ' (none specified)'
|
|
@@ -274,10 +311,11 @@ function generateDefaultManifest(contextFiles = {}) {
|
|
|
274
311
|
public: {
|
|
275
312
|
topics: [{ topic: 'What I do', description: 'Brief professional description' }],
|
|
276
313
|
objectives: [{ objective: 'Networking', description: 'Connect with others in the field' }],
|
|
277
|
-
do_not_discuss: [{ topic: 'Personal details', reason: 'Redirect to direct owner contact' }]
|
|
314
|
+
do_not_discuss: [{ topic: 'Personal details', reason: 'Redirect to direct owner contact' }],
|
|
315
|
+
allowed_tools: ['Read', 'Grep', 'Glob']
|
|
278
316
|
},
|
|
279
|
-
friends: { topics: [], objectives: [], do_not_discuss: [] },
|
|
280
|
-
family: { topics: [], objectives: [], do_not_discuss: [] }
|
|
317
|
+
friends: { topics: [], objectives: [], do_not_discuss: [], allowed_tools: ['Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'] },
|
|
318
|
+
family: { topics: [], objectives: [], do_not_discuss: [], allowed_tools: ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'] }
|
|
281
319
|
},
|
|
282
320
|
never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
|
|
283
321
|
personality_notes: 'Direct and technical. Prefers depth over breadth.'
|
|
@@ -321,17 +359,20 @@ function generateDefaultManifest(contextFiles = {}) {
|
|
|
321
359
|
objectives: publicObjectives.length > 0 ? publicObjectives : [
|
|
322
360
|
{ objective: 'Grow network', description: 'Connect with others working on similar problems' }
|
|
323
361
|
],
|
|
324
|
-
do_not_discuss: [{ topic: 'Personal details', reason: 'Redirect to direct owner contact' }]
|
|
362
|
+
do_not_discuss: [{ topic: 'Personal details', reason: 'Redirect to direct owner contact' }],
|
|
363
|
+
allowed_tools: ['Read', 'Grep', 'Glob']
|
|
325
364
|
},
|
|
326
365
|
friends: {
|
|
327
366
|
topics: friendsTopics,
|
|
328
367
|
objectives: friendsObjectives,
|
|
329
|
-
do_not_discuss: []
|
|
368
|
+
do_not_discuss: [],
|
|
369
|
+
allowed_tools: ['Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch']
|
|
330
370
|
},
|
|
331
371
|
family: {
|
|
332
372
|
topics: familyTopics,
|
|
333
373
|
objectives: [],
|
|
334
|
-
do_not_discuss: []
|
|
374
|
+
do_not_discuss: [],
|
|
375
|
+
allowed_tools: ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch']
|
|
335
376
|
}
|
|
336
377
|
},
|
|
337
378
|
never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
|
|
@@ -385,7 +426,7 @@ function validateDisclosureSubmission(data) {
|
|
|
385
426
|
errors.push(`Unknown tiers: ${extraTiers.join(', ')} — only public, friends, family are allowed`);
|
|
386
427
|
}
|
|
387
428
|
|
|
388
|
-
const LIST_LIMITS = { topics: 15, objectives: 8, do_not_discuss: 10 };
|
|
429
|
+
const LIST_LIMITS = { topics: 15, objectives: 8, do_not_discuss: 10, allowed_tools: 12 };
|
|
389
430
|
|
|
390
431
|
for (const tier of TIER_HIERARCHY) {
|
|
391
432
|
const tierData = tiersData[tier];
|
|
@@ -455,6 +496,28 @@ function validateDisclosureSubmission(data) {
|
|
|
455
496
|
}
|
|
456
497
|
}
|
|
457
498
|
}
|
|
499
|
+
|
|
500
|
+
// Validate allowed_tools array
|
|
501
|
+
if (tierData.allowed_tools !== undefined) {
|
|
502
|
+
if (!Array.isArray(tierData.allowed_tools)) {
|
|
503
|
+
errors.push(`tiers.${tier}.allowed_tools must be an array`);
|
|
504
|
+
} else {
|
|
505
|
+
if (tierData.allowed_tools.length > LIST_LIMITS.allowed_tools) {
|
|
506
|
+
errors.push(`tiers.${tier}.allowed_tools has ${tierData.allowed_tools.length} items — max ${LIST_LIMITS.allowed_tools}`);
|
|
507
|
+
}
|
|
508
|
+
for (let i = 0; i < tierData.allowed_tools.length; i++) {
|
|
509
|
+
const raw = tierData.allowed_tools[i];
|
|
510
|
+
if (typeof raw !== 'string') {
|
|
511
|
+
errors.push(`tiers.${tier}.allowed_tools[${i}] must be a string`);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const canonical = normalizeToolName(raw);
|
|
515
|
+
if (!canonical) {
|
|
516
|
+
errors.push(`tiers.${tier}.allowed_tools[${i}] invalid tool "${raw}" (allowed: ${CANONICAL_TOOL_NAMES.join(', ')})`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
458
521
|
}
|
|
459
522
|
|
|
460
523
|
// Validate never_disclose (optional, defaults to sensible list)
|
|
@@ -506,7 +569,11 @@ function validateDisclosureSubmission(data) {
|
|
|
506
569
|
do_not_discuss: (tiersData[tier].do_not_discuss || []).map(item => ({
|
|
507
570
|
topic: item.topic,
|
|
508
571
|
reason: item.reason || ''
|
|
509
|
-
}))
|
|
572
|
+
})),
|
|
573
|
+
allowed_tools: dedupeStringList(
|
|
574
|
+
(tiersData[tier].allowed_tools || []).map(tool => normalizeToolName(tool)).filter(Boolean),
|
|
575
|
+
80
|
|
576
|
+
)
|
|
510
577
|
};
|
|
511
578
|
}
|
|
512
579
|
|
|
@@ -620,17 +687,20 @@ Use ALL available context to build a reasonable disclosure profile. If truly not
|
|
|
620
687
|
],
|
|
621
688
|
"do_not_discuss": [
|
|
622
689
|
{ "topic": "Topic to avoid", "reason": "Why this should be redirected" }
|
|
623
|
-
]
|
|
690
|
+
],
|
|
691
|
+
"allowed_tools": ["Read", "Grep", "Glob"]
|
|
624
692
|
},
|
|
625
693
|
"friends": {
|
|
626
694
|
"topics": [],
|
|
627
695
|
"objectives": [],
|
|
628
|
-
"do_not_discuss": []
|
|
696
|
+
"do_not_discuss": [],
|
|
697
|
+
"allowed_tools": ["Bash(readonly)", "Read", "Grep", "Glob", "WebSearch", "WebFetch"]
|
|
629
698
|
},
|
|
630
699
|
"family": {
|
|
631
700
|
"topics": [],
|
|
632
701
|
"objectives": [],
|
|
633
|
-
"do_not_discuss": []
|
|
702
|
+
"do_not_discuss": [],
|
|
703
|
+
"allowed_tools": ["Bash", "Read", "Grep", "Glob", "WebSearch", "WebFetch"]
|
|
634
704
|
}
|
|
635
705
|
},
|
|
636
706
|
"never_disclose": ["API keys", "Credentials", "Financial figures"],
|
|
@@ -673,6 +743,12 @@ Family callers see everything. Friends see friends + public. Public callers see
|
|
|
673
743
|
- Sensitive subjects
|
|
674
744
|
- Max 3 per tier
|
|
675
745
|
|
|
746
|
+
**allowed_tools** — Tools this tier can use during calls:
|
|
747
|
+
- Choose only the minimum tools needed for that tier's topics/objectives
|
|
748
|
+
- Use exact tool names: Bash, Bash(readonly), Read, Grep, Glob, WebSearch, WebFetch
|
|
749
|
+
- Public should usually stay read-only
|
|
750
|
+
- Family can include broader tooling when justified
|
|
751
|
+
|
|
676
752
|
Also identify:
|
|
677
753
|
- **never_disclose** — information that should NEVER be shared regardless of tier (API keys, credentials, financial data, etc.)
|
|
678
754
|
- **personality_notes** — a 1-2 sentence description of the owner's communication style
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
const { execSync, spawnSync } = require('child_process');
|
|
14
14
|
const { createLogger } = require('./logger');
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
runClaudeTurn: invokeClaudeTurn,
|
|
17
|
+
buildSubagentSystemPrompt,
|
|
18
|
+
runClaudeSummary,
|
|
19
|
+
resolveClaudeAllowedTools
|
|
20
|
+
} = require('./claude-subagent');
|
|
16
21
|
const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./disclosure');
|
|
17
22
|
const { HARD_FALLBACK_TURN_TIMEOUT_MS } = require('./turn-timeout');
|
|
18
23
|
|
|
@@ -149,7 +154,9 @@ function createRuntimeAdapter(options = {}) {
|
|
|
149
154
|
}
|
|
150
155
|
});
|
|
151
156
|
|
|
152
|
-
// Claude
|
|
157
|
+
// Claude state tracking.
|
|
158
|
+
// Design decision (A2A-29): we keep per-conversation state for prompt/metadata
|
|
159
|
+
// continuity, but Claude execution itself is stateless (no `--resume`).
|
|
153
160
|
const claudeSessions = new Map();
|
|
154
161
|
|
|
155
162
|
async function runClaudeTurnAdapter({ sessionId, message, caller, context, timeoutMs }) {
|
|
@@ -166,7 +173,29 @@ function createRuntimeAdapter(options = {}) {
|
|
|
166
173
|
const tierTopics = getTopicsForTier(context?.tier || 'public');
|
|
167
174
|
const formatted = formatTopicsForPrompt(tierTopics);
|
|
168
175
|
|
|
169
|
-
|
|
176
|
+
session = {
|
|
177
|
+
systemPrompt: '',
|
|
178
|
+
turnCount: 0,
|
|
179
|
+
lastMeta: null,
|
|
180
|
+
// Keep a permission snapshot so summary runs with the same policy envelope.
|
|
181
|
+
permissionSnapshot: {
|
|
182
|
+
capabilities: Array.isArray(context?.capabilities) ? context.capabilities : [],
|
|
183
|
+
allowedTopics: Array.isArray(context?.allowedTopics || context?.allowed_topics)
|
|
184
|
+
? (context?.allowedTopics || context?.allowed_topics)
|
|
185
|
+
: [],
|
|
186
|
+
allowedTools: Array.isArray(context?.allowedTools || context?.allowed_tools)
|
|
187
|
+
? (context?.allowedTools || context?.allowed_tools)
|
|
188
|
+
: []
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const sessionAllowedTools = resolveClaudeAllowedTools({
|
|
193
|
+
capabilities: session.permissionSnapshot.capabilities,
|
|
194
|
+
allowedTopics: session.permissionSnapshot.allowedTopics,
|
|
195
|
+
allowedTools: session.permissionSnapshot.allowedTools
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
session.systemPrompt = buildSubagentSystemPrompt({
|
|
170
199
|
agentName: context?.agentName || 'Agent',
|
|
171
200
|
ownerName: context?.ownerName || 'the owner',
|
|
172
201
|
otherAgentName: caller?.name || 'Remote Agent',
|
|
@@ -177,10 +206,10 @@ function createRuntimeAdapter(options = {}) {
|
|
|
177
206
|
doNotDiscuss: formatted.doNotDiscuss,
|
|
178
207
|
neverDisclose: formatted.neverDisclose,
|
|
179
208
|
personalityNotes: manifest.personality_notes || '',
|
|
180
|
-
roleContext: context?.roleContext || ''
|
|
209
|
+
roleContext: context?.roleContext || '',
|
|
210
|
+
allowedTools: sessionAllowedTools
|
|
181
211
|
});
|
|
182
212
|
|
|
183
|
-
session = { claudeSessionId: null, systemPrompt, turnCount: 0, lastMeta: null };
|
|
184
213
|
claudeSessions.set(sessionId, session);
|
|
185
214
|
}
|
|
186
215
|
|
|
@@ -199,7 +228,6 @@ function createRuntimeAdapter(options = {}) {
|
|
|
199
228
|
});
|
|
200
229
|
|
|
201
230
|
const result = await invokeClaudeTurn({
|
|
202
|
-
sessionId: session.claudeSessionId,
|
|
203
231
|
systemPrompt: session.systemPrompt,
|
|
204
232
|
turnMessage: message,
|
|
205
233
|
turn: session.turnCount,
|
|
@@ -209,12 +237,27 @@ function createRuntimeAdapter(options = {}) {
|
|
|
209
237
|
activeThreads: context?.activeThreads || [],
|
|
210
238
|
candidateCollaborations: context?.candidateCollaborations || [],
|
|
211
239
|
closeSignal: context?.closeSignal || false,
|
|
240
|
+
capabilities: Array.isArray(context?.capabilities)
|
|
241
|
+
? context.capabilities
|
|
242
|
+
: (session.permissionSnapshot?.capabilities || []),
|
|
243
|
+
allowedTopics: Array.isArray(context?.allowedTopics || context?.allowed_topics)
|
|
244
|
+
? (context?.allowedTopics || context?.allowed_topics)
|
|
245
|
+
: (session.permissionSnapshot?.allowedTopics || []),
|
|
246
|
+
allowedTools: Array.isArray(context?.allowedTools || context?.allowed_tools)
|
|
247
|
+
? (context?.allowedTools || context?.allowed_tools)
|
|
248
|
+
: (session.permissionSnapshot?.allowedTools || []),
|
|
212
249
|
timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
213
250
|
});
|
|
214
251
|
|
|
215
|
-
//
|
|
216
|
-
if (
|
|
217
|
-
session.
|
|
252
|
+
// Update permission snapshot if the caller supplied explicit context this turn.
|
|
253
|
+
if (Array.isArray(context?.capabilities)) {
|
|
254
|
+
session.permissionSnapshot.capabilities = context.capabilities;
|
|
255
|
+
}
|
|
256
|
+
if (Array.isArray(context?.allowedTopics || context?.allowed_topics)) {
|
|
257
|
+
session.permissionSnapshot.allowedTopics = context?.allowedTopics || context?.allowed_topics;
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(context?.allowedTools || context?.allowed_tools)) {
|
|
260
|
+
session.permissionSnapshot.allowedTools = context?.allowedTools || context?.allowed_tools;
|
|
218
261
|
}
|
|
219
262
|
|
|
220
263
|
// Store flags/state for retrieval via getLastTurnMeta
|
|
@@ -385,20 +428,33 @@ function createRuntimeAdapter(options = {}) {
|
|
|
385
428
|
const requestId = callerInfo?.request_id || callerInfo?.requestId;
|
|
386
429
|
const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
|
|
387
430
|
|
|
388
|
-
// Claude mode:
|
|
431
|
+
// Claude mode: stateless summary invocation (no session restore dependency).
|
|
389
432
|
if (modeInfo.mode === 'claude') {
|
|
390
433
|
const session = claudeSessions.get(sessionId);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
434
|
+
const capabilities = session?.permissionSnapshot?.capabilities
|
|
435
|
+
|| callerInfo?.capabilities
|
|
436
|
+
|| [];
|
|
437
|
+
const allowedTopics = session?.permissionSnapshot?.allowedTopics
|
|
438
|
+
|| callerInfo?.allowedTopics
|
|
439
|
+
|| callerInfo?.allowed_topics
|
|
440
|
+
|| [];
|
|
441
|
+
const allowedTools = session?.permissionSnapshot?.allowedTools
|
|
442
|
+
|| callerInfo?.allowedTools
|
|
443
|
+
|| callerInfo?.allowed_tools
|
|
444
|
+
|| [];
|
|
445
|
+
|
|
446
|
+
const result = await runClaudeSummary({
|
|
447
|
+
prompt,
|
|
448
|
+
reason: 'conversation ended',
|
|
449
|
+
capabilities,
|
|
450
|
+
allowedTopics,
|
|
451
|
+
allowedTools,
|
|
452
|
+
timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
453
|
+
});
|
|
454
|
+
if (result && result.summary) {
|
|
455
|
+
return result;
|
|
400
456
|
}
|
|
401
|
-
throw new Error('Claude summary
|
|
457
|
+
throw new Error('Claude summary returned empty result');
|
|
402
458
|
}
|
|
403
459
|
|
|
404
460
|
if (modeInfo.mode !== 'openclaw') {
|
package/src/lib/tokens.js
CHANGED
|
@@ -201,6 +201,7 @@ class TokenStore {
|
|
|
201
201
|
// Snapshot of actual capabilities at creation time
|
|
202
202
|
allowedTopics = null, // Array of topic strings, e.g. ['chat', 'calendar.read']
|
|
203
203
|
allowedGoals = null, // Array of goal strings, e.g. ['grow-network', 'find-collaborators']
|
|
204
|
+
allowedTools = null, // Array of tool names, e.g. ['Read', 'Grep', 'Glob']
|
|
204
205
|
tierSettings = null, // Object with tier-specific settings
|
|
205
206
|
timeoutMs = null
|
|
206
207
|
} = options;
|
|
@@ -246,6 +247,14 @@ class TokenStore {
|
|
|
246
247
|
'custom': configTiers.custom?.goals || []
|
|
247
248
|
};
|
|
248
249
|
|
|
250
|
+
// Default tool allowlist based on tier label (snapshot at creation).
|
|
251
|
+
const defaultTools = {
|
|
252
|
+
'public': configTiers.public?.allowed_tools || TokenStore.DEFAULT_ALLOWED_TOOLS.public,
|
|
253
|
+
'friends': configTiers.friends?.allowed_tools || TokenStore.DEFAULT_ALLOWED_TOOLS.friends,
|
|
254
|
+
'family': configTiers.family?.allowed_tools || TokenStore.DEFAULT_ALLOWED_TOOLS.family,
|
|
255
|
+
'custom': configTiers.custom?.allowed_tools || TokenStore.DEFAULT_ALLOWED_TOOLS.custom
|
|
256
|
+
};
|
|
257
|
+
|
|
249
258
|
// Resolve capabilities: explicit > config > defaults
|
|
250
259
|
const defaultCapabilities = (configTiers[tier]?.capabilities?.length)
|
|
251
260
|
? configTiers[tier].capabilities
|
|
@@ -261,6 +270,7 @@ class TokenStore {
|
|
|
261
270
|
capabilities: capabilities || defaultCapabilities,
|
|
262
271
|
allowed_topics: allowedTopics || defaultTopics[tier] || ['chat'],
|
|
263
272
|
allowed_goals: allowedGoals || defaultGoals[tier] || [],
|
|
273
|
+
allowed_tools: allowedTools || defaultTools[tier] || TokenStore.DEFAULT_ALLOWED_TOOLS.public,
|
|
264
274
|
timeout_ms: parsePositiveTimeoutMs(timeoutMs),
|
|
265
275
|
tier_settings: tierSettings || {}, // Snapshot of settings at creation
|
|
266
276
|
disclosure,
|
|
@@ -346,6 +356,7 @@ class TokenStore {
|
|
|
346
356
|
capabilities,
|
|
347
357
|
allowed_topics: record.allowed_topics || ['chat'],
|
|
348
358
|
allowed_goals: record.allowed_goals || [],
|
|
359
|
+
allowed_tools: record.allowed_tools || TokenStore.DEFAULT_ALLOWED_TOOLS[tier] || TokenStore.DEFAULT_ALLOWED_TOOLS.public,
|
|
349
360
|
timeout_ms: timeoutMs,
|
|
350
361
|
tier_settings: record.tier_settings || {},
|
|
351
362
|
disclosure: record.disclosure,
|
|
@@ -777,4 +788,11 @@ TokenStore.DEFAULT_CAPABILITIES = {
|
|
|
777
788
|
'custom': ['context-read']
|
|
778
789
|
};
|
|
779
790
|
|
|
791
|
+
TokenStore.DEFAULT_ALLOWED_TOOLS = {
|
|
792
|
+
'public': ['Read', 'Grep', 'Glob'],
|
|
793
|
+
'friends': ['Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'],
|
|
794
|
+
'family': ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'],
|
|
795
|
+
'custom': ['Read', 'Grep', 'Glob']
|
|
796
|
+
};
|
|
797
|
+
|
|
780
798
|
module.exports = { TokenStore };
|
package/src/routes/a2a.js
CHANGED
|
@@ -347,6 +347,8 @@ function createRoutes(options = {}) {
|
|
|
347
347
|
tier: validation.tier,
|
|
348
348
|
capabilities: validation.capabilities,
|
|
349
349
|
allowed_topics: validation.allowed_topics,
|
|
350
|
+
allowed_goals: validation.allowed_goals,
|
|
351
|
+
allowed_tools: validation.allowed_tools,
|
|
350
352
|
timeout_ms: validation.timeout_ms,
|
|
351
353
|
disclosure: validation.disclosure,
|
|
352
354
|
caller: sanitizedCaller,
|
|
@@ -390,6 +392,11 @@ function createRoutes(options = {}) {
|
|
|
390
392
|
if (monitor) {
|
|
391
393
|
monitor.trackActivity(a2aContext.conversation_id, {
|
|
392
394
|
...sanitizedCaller,
|
|
395
|
+
tier: validation.tier,
|
|
396
|
+
capabilities: validation.capabilities,
|
|
397
|
+
allowed_topics: validation.allowed_topics,
|
|
398
|
+
allowed_goals: validation.allowed_goals,
|
|
399
|
+
allowed_tools: validation.allowed_tools,
|
|
393
400
|
trace_id: traceId,
|
|
394
401
|
request_id: requestId
|
|
395
402
|
});
|
package/src/routes/dashboard.js
CHANGED
|
@@ -170,16 +170,18 @@ function parseTopicObjects(values) {
|
|
|
170
170
|
return cleaned;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
function formatInviteMessage({ owner, agentName, inviteUrl, topics, goals, expiresText }) {
|
|
173
|
+
function formatInviteMessage({ owner, agentName, inviteUrl, topics, goals, tools, expiresText }) {
|
|
174
174
|
const ownerText = owner || 'Someone';
|
|
175
175
|
const topicsList = topics.length > 0 ? topics.join(' · ') : 'chat';
|
|
176
176
|
const goalsList = (goals || []).join(' · ');
|
|
177
|
+
const toolsList = (tools || []).join(' · ');
|
|
177
178
|
const expirationLine = expiresText === 'never' ? '' : `\n⏰ ${expiresText}`;
|
|
178
179
|
return `📞🗣️ **Agent-to-Agent Call Invite**
|
|
179
180
|
|
|
180
181
|
👤 **${ownerText}** would like your agent to call **${agentName}** and explore where our owners might collaborate.
|
|
181
182
|
|
|
182
183
|
💬 ${topicsList}${goalsList ? `\n🎯 ${goalsList}` : ''}
|
|
184
|
+
${toolsList ? `\n🧰 ${toolsList}` : ''}
|
|
183
185
|
|
|
184
186
|
${inviteUrl}${expirationLine}
|
|
185
187
|
|
|
@@ -1271,6 +1273,7 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1271
1273
|
name: configTier.name || tierId,
|
|
1272
1274
|
description: configTier.description || '',
|
|
1273
1275
|
capabilities: configTier.capabilities || [],
|
|
1276
|
+
allowed_tools: sanitizeStringArray(configTier.allowed_tools || [], 30, 80),
|
|
1274
1277
|
topics: sanitizeStringArray(configTier.topics || []),
|
|
1275
1278
|
goals: sanitizeStringArray(configTier.goals || []),
|
|
1276
1279
|
disclosure: configTier.disclosure || 'minimal',
|
|
@@ -1309,6 +1312,7 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1309
1312
|
if (body.description !== undefined) update.description = sanitizeString(body.description, 300);
|
|
1310
1313
|
if (body.disclosure !== undefined) update.disclosure = sanitizeString(body.disclosure, 40) || 'minimal';
|
|
1311
1314
|
if (body.capabilities !== undefined) update.capabilities = sanitizeStringArray(body.capabilities, 100, 120);
|
|
1315
|
+
if (body.allowed_tools !== undefined) update.allowed_tools = sanitizeStringArray(body.allowed_tools, 30, 80);
|
|
1312
1316
|
if (body.examples !== undefined) update.examples = sanitizeStringArray(body.examples, 20, 120);
|
|
1313
1317
|
if (body.topics !== undefined) update.topics = sanitizeStringArray(body.topics, 200, 160);
|
|
1314
1318
|
if (body.goals !== undefined) update.goals = sanitizeStringArray(body.goals, 200, 160);
|
|
@@ -1368,6 +1372,7 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1368
1372
|
name: sanitizeString(body.name || tierId, 120),
|
|
1369
1373
|
description: sanitizeString(body.description || 'Custom tier', 300),
|
|
1370
1374
|
capabilities: sanitizeStringArray(body.capabilities || []),
|
|
1375
|
+
allowed_tools: sanitizeStringArray(body.allowed_tools || [], 30, 80),
|
|
1371
1376
|
topics: sanitizeStringArray(body.topics || []),
|
|
1372
1377
|
goals: sanitizeStringArray(body.goals || []),
|
|
1373
1378
|
disclosure: sanitizeString(body.disclosure || 'minimal', 40),
|
|
@@ -1464,6 +1469,7 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1464
1469
|
|
|
1465
1470
|
const allowedTopics = sanitizeStringArray(body.topics || tier.topics || []);
|
|
1466
1471
|
const allowedGoals = sanitizeStringArray(body.goals || tier.goals || []);
|
|
1472
|
+
const allowedTools = sanitizeStringArray(body.tools || body.allowed_tools || tier.allowed_tools || [], 30, 80);
|
|
1467
1473
|
const { token, record } = context.tokenStore.create({
|
|
1468
1474
|
name,
|
|
1469
1475
|
owner,
|
|
@@ -1474,6 +1480,7 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1474
1480
|
maxCalls,
|
|
1475
1481
|
allowedTopics: allowedTopics.length ? allowedTopics : null,
|
|
1476
1482
|
allowedGoals: allowedGoals.length ? allowedGoals : null,
|
|
1483
|
+
allowedTools: allowedTools.length ? allowedTools : null,
|
|
1477
1484
|
tierSettings: {
|
|
1478
1485
|
tierId,
|
|
1479
1486
|
...tier
|
|
@@ -1495,6 +1502,7 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1495
1502
|
inviteUrl,
|
|
1496
1503
|
topics: record.allowed_topics || [],
|
|
1497
1504
|
goals: record.allowed_goals || [],
|
|
1505
|
+
tools: record.allowed_tools || [],
|
|
1498
1506
|
expiresText
|
|
1499
1507
|
});
|
|
1500
1508
|
|
package/src/server.js
CHANGED
|
@@ -621,13 +621,44 @@ async function callAgent(message, a2aContext) {
|
|
|
621
621
|
conversationId,
|
|
622
622
|
tier: tierInfo,
|
|
623
623
|
ownerName: agentContext.owner,
|
|
624
|
+
capabilities: Array.isArray(a2aContext.capabilities) ? a2aContext.capabilities : [],
|
|
624
625
|
allowedTopics: a2aContext.allowed_topics || [],
|
|
626
|
+
allowed_topics: a2aContext.allowed_topics || [],
|
|
627
|
+
allowedGoals: a2aContext.allowed_goals || [],
|
|
628
|
+
allowed_goals: a2aContext.allowed_goals || [],
|
|
629
|
+
allowedTools: a2aContext.allowed_tools || [],
|
|
630
|
+
allowed_tools: a2aContext.allowed_tools || [],
|
|
625
631
|
timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
|
|
626
632
|
traceId,
|
|
627
633
|
requestId
|
|
628
634
|
}
|
|
629
635
|
});
|
|
630
636
|
|
|
637
|
+
// Claude mode returns structured metadata (statePatch + flags) via side channel.
|
|
638
|
+
// We persist owner-facing flags so permission questions (e.g. blocked tool requests)
|
|
639
|
+
// are visible in call history even though the remote only sees conversational text.
|
|
640
|
+
const turnMeta = runtime.mode === 'claude' && typeof runtime.getLastTurnMeta === 'function'
|
|
641
|
+
? runtime.getLastTurnMeta(sessionId)
|
|
642
|
+
: null;
|
|
643
|
+
if (turnMeta?.flags?.length > 0) {
|
|
644
|
+
const convStoreForFlags = getServerConvStore();
|
|
645
|
+
if (convStoreForFlags) {
|
|
646
|
+
try {
|
|
647
|
+
convStoreForFlags.addMessage(conversationId, {
|
|
648
|
+
direction: 'outbound',
|
|
649
|
+
role: 'system',
|
|
650
|
+
content: `[flags] ${turnMeta.flags.map(f => f.content || f.type).join('; ')}`,
|
|
651
|
+
metadata: JSON.stringify({ flags: turnMeta.flags, phase: collabState.phase })
|
|
652
|
+
});
|
|
653
|
+
} catch (err) {
|
|
654
|
+
callLogger.warn('Failed to persist owner flags for turn', {
|
|
655
|
+
event: 'call_turn_flags_persist_failed',
|
|
656
|
+
error: err
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
631
662
|
if (collabMode !== 'adaptive') {
|
|
632
663
|
return rawResponse;
|
|
633
664
|
}
|
|
@@ -637,7 +668,17 @@ async function callAgent(message, a2aContext) {
|
|
|
637
668
|
const beforeTurn = collabState.turnCount;
|
|
638
669
|
let usedMetadata = false;
|
|
639
670
|
|
|
640
|
-
|
|
671
|
+
// Prefer explicit Claude metadata side channel in claude mode.
|
|
672
|
+
if (turnMeta?.statePatch) {
|
|
673
|
+
usedMetadata = applyCollaborationPatch(collabState, turnMeta.statePatch);
|
|
674
|
+
if (!usedMetadata) {
|
|
675
|
+
callLogger.warn('Invalid collaboration patch; applying fallback heuristics', {
|
|
676
|
+
event: 'collaboration_patch_invalid',
|
|
677
|
+
error_code: 'COLLABORATION_PATCH_INVALID',
|
|
678
|
+
hint: 'Ensure assistant emits valid collaboration metadata JSON block.'
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
} else if (parsed.hasState && parsed.statePatch) {
|
|
641
682
|
usedMetadata = applyCollaborationPatch(collabState, parsed.statePatch);
|
|
642
683
|
if (!usedMetadata) {
|
|
643
684
|
callLogger.warn('Invalid collaboration patch; applying fallback heuristics', {
|
|
@@ -770,8 +811,9 @@ async function generateSummary(messages, callerInfo) {
|
|
|
770
811
|
});
|
|
771
812
|
|
|
772
813
|
try {
|
|
814
|
+
const summarySessionId = conversationId ? `a2a-${conversationId}` : `summary-${Date.now()}`;
|
|
773
815
|
return await runtime.summarize({
|
|
774
|
-
sessionId:
|
|
816
|
+
sessionId: summarySessionId,
|
|
775
817
|
prompt,
|
|
776
818
|
messages,
|
|
777
819
|
callerInfo,
|