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.
@@ -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: `summary-${Date.now()}`,
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;
@@ -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 { runClaudeTurn: invokeClaudeTurn, buildSubagentSystemPrompt, runClaudeSummary } = require('./claude-subagent');
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 subagent session tracking
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
- const systemPrompt = buildSubagentSystemPrompt({
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
- // Store session ID from first turn for subsequent --resume
216
- if (result.sessionId) {
217
- session.claudeSessionId = result.sessionId;
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: use the subagent session for summarization
431
+ // Claude mode: stateless summary invocation (no session restore dependency).
389
432
  if (modeInfo.mode === 'claude') {
390
433
  const session = claudeSessions.get(sessionId);
391
- if (session?.claudeSessionId) {
392
- const result = await runClaudeSummary(
393
- session.claudeSessionId,
394
- 'conversation ended',
395
- timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
396
- );
397
- if (result && result.summary) {
398
- return result;
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 session not available or returned empty result');
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
  });
@@ -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
- if (parsed.hasState && parsed.statePatch) {
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: `summary-${Date.now()}`,
816
+ sessionId: summarySessionId,
775
817
  prompt,
776
818
  messages,
777
819
  callerInfo,