a2acalling 0.6.52 → 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.
@@ -44,7 +44,10 @@ pub fn start_server() -> StartResult {
44
44
  }
45
45
  };
46
46
 
47
- let port = crate::discovery::read_config_port().unwrap_or(3001);
47
+ let port = crate::discovery::read_config_ports()
48
+ .first()
49
+ .copied()
50
+ .unwrap_or(3001);
48
51
  let port_str = port.to_string();
49
52
 
50
53
  let result = Command::new(&binary)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.52",
3
+ "version": "0.6.53",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1056,6 +1056,7 @@ function renderTierEditor(tierId) {
1056
1056
  document.getElementById('tier-name').value = tier.name || tier.id;
1057
1057
  document.getElementById('tier-description').value = tier.description || '';
1058
1058
  document.getElementById('tier-disclosure').value = tier.disclosure || 'minimal';
1059
+ document.getElementById('tier-tools').value = toLines(tier.allowed_tools || []);
1059
1060
  document.getElementById('tier-topics').value = toLines(tier.topics || []);
1060
1061
  document.getElementById('tier-goals').value = toLines(tier.goals || []);
1061
1062
  }
@@ -1072,6 +1073,7 @@ function bindSettingsActions() {
1072
1073
  name: document.getElementById('tier-name').value,
1073
1074
  description: document.getElementById('tier-description').value,
1074
1075
  disclosure: document.getElementById('tier-disclosure').value,
1076
+ allowed_tools: fromLines(document.getElementById('tier-tools').value),
1075
1077
  topics: fromLines(document.getElementById('tier-topics').value),
1076
1078
  goals: fromLines(document.getElementById('tier-goals').value)
1077
1079
  };
@@ -132,6 +132,7 @@
132
132
  <label>Name <input id="tier-name" type="text"></label>
133
133
  <label>Description <input id="tier-description" type="text"></label>
134
134
  <label>Disclosure <input id="tier-disclosure" type="text" placeholder="minimal"></label>
135
+ <label>Allowed Tools (one per line)<textarea id="tier-tools" rows="5" placeholder="Read&#10;Grep&#10;Glob"></textarea></label>
135
136
  <label>Topics (one per line)<textarea id="tier-topics" rows="6"></textarea></label>
136
137
  <label>Goals (one per line)<textarea id="tier-goals" rows="6"></textarea></label>
137
138
  <div class="row">
@@ -10,8 +10,8 @@
10
10
  * Stateless calls cost more tokens but are operationally safer under load.
11
11
  *
12
12
  * Permissioning is still enforced:
13
- * `--allowedTools` is derived per request from token capabilities + allowed topics.
14
- * We do not hardcode one universal allowlist anymore.
13
+ * `--allowedTools` is derived per request from token capabilities + allowed topics
14
+ * and can be further constrained by per-tier `allowed_tools` policy from onboarding.
15
15
  */
16
16
 
17
17
  const { execSync, spawn } = require('child_process');
@@ -22,8 +22,43 @@ const logger = createLogger({ component: 'a2a.claude-subagent' });
22
22
 
23
23
  const A2A_RESPONSE_REGEX = /<a2a_response>\s*([\s\S]*?)\s*<\/a2a_response>/i;
24
24
  const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-5-20250929';
25
+ const CLAUDE_TOOL_UNIVERSE = ['Bash', 'Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
25
26
  const LEGACY_DEFAULT_TOOLS = ['Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
26
27
 
28
+ const TOOL_NAME_MAP = {
29
+ bash: 'Bash',
30
+ 'bash(readonly)': 'Bash(readonly)',
31
+ 'bash-readonly': 'Bash(readonly)',
32
+ read: 'Read',
33
+ grep: 'Grep',
34
+ glob: 'Glob',
35
+ websearch: 'WebSearch',
36
+ webfetch: 'WebFetch'
37
+ };
38
+
39
+ function normalizeToolName(value) {
40
+ const key = String(value || '').trim().toLowerCase();
41
+ return TOOL_NAME_MAP[key] || null;
42
+ }
43
+
44
+ function sanitizeAllowedToolList(values) {
45
+ if (!Array.isArray(values)) return [];
46
+ const out = [];
47
+ const seen = new Set();
48
+ for (const value of values) {
49
+ const canonical = normalizeToolName(value);
50
+ if (!canonical || seen.has(canonical)) continue;
51
+ seen.add(canonical);
52
+ out.push(canonical);
53
+ }
54
+ return out;
55
+ }
56
+
57
+ function formatToolListForPrompt(tools) {
58
+ if (!Array.isArray(tools) || tools.length === 0) return ' (none)';
59
+ return tools.map(tool => ` - ${tool}`).join('\n');
60
+ }
61
+
27
62
  /**
28
63
  * Check if `claude` CLI is available in PATH.
29
64
  */
@@ -49,6 +84,7 @@ function isClaudeAvailable() {
49
84
  * @param {string} config.tierObjectives - formatted objectives string
50
85
  * @param {string} config.doNotDiscuss - formatted do_not_discuss string
51
86
  * @param {string} config.neverDisclose - formatted never_disclose string
87
+ * @param {string[]} [config.allowedTools] - Effective tool allowlist for this call
52
88
  * @param {string} config.personalityNotes
53
89
  * @param {string} config.roleContext
54
90
  * @returns {string}
@@ -64,10 +100,17 @@ function buildSubagentSystemPrompt(config) {
64
100
  tierObjectives = ' (none specified)',
65
101
  doNotDiscuss = ' (none specified)',
66
102
  neverDisclose = ' (none specified)',
103
+ allowedTools = [],
67
104
  personalityNotes = '',
68
105
  roleContext = ''
69
106
  } = config;
70
107
 
108
+ const effectiveAllowedTools = sanitizeAllowedToolList(allowedTools);
109
+ const allowedForPrompt = effectiveAllowedTools.length > 0
110
+ ? effectiveAllowedTools
111
+ : [...LEGACY_DEFAULT_TOOLS];
112
+ const blockedTools = CLAUDE_TOOL_UNIVERSE.filter(tool => !allowedForPrompt.includes(tool));
113
+
71
114
  return `You are ${agentName}, the personal AI agent for ${ownerName}.
72
115
  You are on a live A2A (agent-to-agent) call with ${otherAgentName}, who represents ${otherOwnerName}. ${roleContext}
73
116
 
@@ -108,6 +151,19 @@ ${doNotDiscuss}
108
151
  NEVER disclose:
109
152
  ${neverDisclose}
110
153
 
154
+ == TOOL PERMISSIONS ==
155
+
156
+ Allowed tools this call:
157
+ ${formatToolListForPrompt(allowedForPrompt)}
158
+
159
+ Blocked tools this call:
160
+ ${formatToolListForPrompt(blockedTools)}
161
+
162
+ Tool policy:
163
+ - Never invoke blocked tools.
164
+ - If you need a blocked tool, continue the conversation without it and add a "question_for_owner" flag requesting permission.
165
+ - Include exact tool name and reason in the flag content.
166
+
111
167
  == BEHAVIORAL MANDATE ==
112
168
 
113
169
  You operate in three concurrent modes:
@@ -228,13 +284,11 @@ function parseSubagentResponse(resultText) {
228
284
  */
229
285
  function spawnClaude(args, timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS) {
230
286
  return new Promise((resolve, reject) => {
287
+ const spawnEnv = { ...process.env, FORCE_COLOR: '0' };
288
+ delete spawnEnv.CLAUDECODE;
231
289
  const proc = spawn('claude', args, {
232
- stdio: ['pipe', 'pipe', 'pipe'],
233
- env: {
234
- ...process.env,
235
- FORCE_COLOR: '0',
236
- CLAUDECODE: '' // Unset to allow nested invocation
237
- }
290
+ stdio: ['ignore', 'pipe', 'pipe'],
291
+ env: spawnEnv
238
292
  });
239
293
 
240
294
  let stdout = '';
@@ -310,16 +364,7 @@ function hasPermissionMatch(values, key) {
310
364
  return values.some(value => value === key || value.startsWith(`${key}.`));
311
365
  }
312
366
 
313
- /**
314
- * Resolve Claude tool allowlist from token-derived permissions.
315
- *
316
- * Notes:
317
- * - We preserve legacy behavior when no permission context is provided, because
318
- * outbound CLI flows may run without token metadata.
319
- * - When permissions are present, we derive tools deterministically so runtime
320
- * allowlists remain variable and auditable per token.
321
- */
322
- function resolveClaudeAllowedTools({ capabilities = [], allowedTopics = [] } = {}) {
367
+ function deriveClaudeToolsFromPermissionSignals({ capabilities = [], allowedTopics = [] } = {}) {
323
368
  const normalizedCaps = normalizePermissionList(capabilities);
324
369
  const normalizedTopics = normalizePermissionList(allowedTopics);
325
370
  const hasPermissionContext = normalizedCaps.length > 0 || normalizedTopics.length > 0;
@@ -367,6 +412,29 @@ function resolveClaudeAllowedTools({ capabilities = [], allowedTopics = [] } = {
367
412
  return tools;
368
413
  }
369
414
 
415
+ /**
416
+ * Resolve Claude tool allowlist from token-derived permissions and explicit per-tier tool policy.
417
+ */
418
+ function resolveClaudeAllowedTools({ capabilities = [], allowedTopics = [], allowedTools = [] } = {}) {
419
+ const derivedTools = deriveClaudeToolsFromPermissionSignals({ capabilities, allowedTopics });
420
+ const explicitTools = sanitizeAllowedToolList(allowedTools);
421
+
422
+ if (explicitTools.length > 0) {
423
+ const hasPermissionSignals = normalizePermissionList(capabilities).length > 0
424
+ || normalizePermissionList(allowedTopics).length > 0;
425
+
426
+ if (hasPermissionSignals) {
427
+ // Permission signals define the ceiling; explicit tier tools can narrow it.
428
+ const narrowed = explicitTools.filter(tool => derivedTools.includes(tool));
429
+ return narrowed.length > 0 ? narrowed : derivedTools;
430
+ }
431
+
432
+ return explicitTools;
433
+ }
434
+
435
+ return derivedTools;
436
+ }
437
+
370
438
  function buildClaudeToolArg(allowedTools) {
371
439
  return Array.isArray(allowedTools) ? allowedTools.join(' ').trim() : '';
372
440
  }
@@ -447,6 +515,7 @@ function summarizeFromPayload(payload, fallbackText) {
447
515
  * @param {boolean} options.closeSignal - Whether close has been signaled
448
516
  * @param {Array<string>} [options.capabilities] - Token capabilities (permission source of truth)
449
517
  * @param {Array<string>} [options.allowedTopics] - Token allowed topics (permission source of truth)
518
+ * @param {Array<string>} [options.allowedTools] - Token allowed tools (onboarding tier policy)
450
519
  * @param {function} [options.spawnFn] - Injectable process runner for tests
451
520
  * @param {number} [options.timeoutMs=300000] - Timeout in milliseconds
452
521
  * @returns {Promise<{ message: string, statePatch: object|null, flags: array }>}
@@ -464,6 +533,7 @@ async function runClaudeTurn(options) {
464
533
  closeSignal = false,
465
534
  capabilities = [],
466
535
  allowedTopics = [],
536
+ allowedTools = [],
467
537
  spawnFn = spawnClaude,
468
538
  timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
469
539
  } = options;
@@ -480,8 +550,8 @@ async function runClaudeTurn(options) {
480
550
  });
481
551
 
482
552
  const startAt = Date.now();
483
- const allowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics });
484
- const allowedToolsArg = buildClaudeToolArg(allowedTools);
553
+ const effectiveAllowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics, allowedTools });
554
+ const allowedToolsArg = buildClaudeToolArg(effectiveAllowedTools);
485
555
  const args = [
486
556
  '-p',
487
557
  '--output-format', 'json',
@@ -494,7 +564,7 @@ async function runClaudeTurn(options) {
494
564
  if (allowedToolsArg) {
495
565
  args.push('--allowedTools', allowedToolsArg);
496
566
  }
497
- args.push(turnPrompt);
567
+ args.push('--', turnPrompt);
498
568
 
499
569
  logger.debug('Spawning Claude subagent turn', {
500
570
  event: 'subagent_turn_start',
@@ -503,7 +573,7 @@ async function runClaudeTurn(options) {
503
573
  max_turns: maxTurns,
504
574
  phase,
505
575
  is_stateless: true,
506
- allowed_tools: allowedTools,
576
+ allowed_tools: effectiveAllowedTools,
507
577
  timeout_ms: timeoutMs
508
578
  }
509
579
  });
@@ -538,8 +608,9 @@ async function runClaudeTurn(options) {
538
608
  * @param {string} [options.reason] - Why the conversation is ending
539
609
  * @param {Array<string>} [options.capabilities] - Token capabilities for summary turn tooling
540
610
  * @param {Array<string>} [options.allowedTopics] - Token allowed topics for summary turn tooling
611
+ * @param {Array<string>} [options.allowedTools] - Token allowed tools for summary turn tooling
541
612
  * @param {function} [options.spawnFn] - Injectable process runner for tests
542
- * @param {number} [timeoutMs=300000] - Timeout in milliseconds
613
+ * @param {number} [options.timeoutMs=300000] - Timeout in milliseconds
543
614
  * @returns {Promise<{ summary: string, ownerSummary: string, actionItems: array, flags: array }>}
544
615
  */
545
616
  async function runClaudeSummary(options = {}) {
@@ -548,6 +619,7 @@ async function runClaudeSummary(options = {}) {
548
619
  reason,
549
620
  capabilities = [],
550
621
  allowedTopics = [],
622
+ allowedTools = [],
551
623
  spawnFn = spawnClaude,
552
624
  timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
553
625
  } = options;
@@ -557,13 +629,13 @@ async function runClaudeSummary(options = {}) {
557
629
  throw new Error('Cannot summarize without a prompt');
558
630
  }
559
631
 
560
- const allowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics });
561
- const allowedToolsArg = buildClaudeToolArg(allowedTools);
632
+ const effectiveAllowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics, allowedTools });
633
+ const allowedToolsArg = buildClaudeToolArg(effectiveAllowedTools);
562
634
 
563
635
  const args = [
564
636
  '-p',
565
637
  '--output-format', 'json',
566
- '--model', DEFAULT_CLAUDE_MODEL,
638
+ '--model', DEFAULT_CLAUDE_MODEL
567
639
  ];
568
640
 
569
641
  if (allowedToolsArg) {
@@ -572,6 +644,7 @@ async function runClaudeSummary(options = {}) {
572
644
  args.push(
573
645
  '--append-system-prompt',
574
646
  `Conversation summary mode. Reason: ${reason || 'conversation ended'}. Return only structured summary JSON.`,
647
+ '--',
575
648
  summaryPrompt
576
649
  );
577
650
 
@@ -581,7 +654,7 @@ async function runClaudeSummary(options = {}) {
581
654
  event: 'subagent_summary_start',
582
655
  data: {
583
656
  reason: reason || 'conversation ended',
584
- allowed_tools: allowedTools
657
+ allowed_tools: effectiveAllowedTools
585
658
  }
586
659
  });
587
660
 
package/src/lib/config.js CHANGED
@@ -125,6 +125,13 @@ function validateTierPatch(tierName, tierConfig) {
125
125
  });
126
126
  }
127
127
 
128
+ if (tierConfig.allowed_tools !== undefined) {
129
+ out.allowed_tools = validateStringArray(tierConfig.allowed_tools, `${tierName}.allowed_tools`, {
130
+ maxItems: 30,
131
+ itemMaxLength: 80
132
+ });
133
+ }
134
+
128
135
  if (tierConfig.topics !== undefined) {
129
136
  out.topics = validateStringArray(tierConfig.topics, `${tierName}.topics`, {
130
137
  maxItems: 200,
@@ -181,6 +188,7 @@ const DEFAULT_CONFIG = {
181
188
  name: 'Public',
182
189
  description: 'Basic networking - safe for anyone',
183
190
  capabilities: ['context-read'],
191
+ allowed_tools: ['Read', 'Grep', 'Glob'],
184
192
  topics: ['chat'],
185
193
  goals: [],
186
194
  disclosure: 'minimal',
@@ -190,6 +198,7 @@ const DEFAULT_CONFIG = {
190
198
  name: 'Friends',
191
199
  description: 'Most capabilities, no sensitive financial data',
192
200
  capabilities: ['context-read', 'calendar.read', 'email.read', 'search'],
201
+ allowed_tools: ['Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'],
193
202
  topics: ['chat', 'search', 'openclaw', 'a2a'],
194
203
  goals: [],
195
204
  disclosure: 'public',
@@ -199,6 +208,7 @@ const DEFAULT_CONFIG = {
199
208
  name: 'Family',
200
209
  description: 'Full access - only for your inner circle',
201
210
  capabilities: ['context-read', 'calendar', 'email', 'search', 'tools', 'memory'],
211
+ allowed_tools: ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'],
202
212
  topics: ['chat', 'search', 'openclaw', 'a2a', 'tools', 'memory'],
203
213
  goals: [],
204
214
  disclosure: 'public',
@@ -208,6 +218,7 @@ const DEFAULT_CONFIG = {
208
218
  name: 'Custom',
209
219
  description: 'User-defined permissions',
210
220
  capabilities: ['context-read'],
221
+ allowed_tools: ['Read', 'Grep', 'Glob'],
211
222
  topics: [],
212
223
  goals: [],
213
224
  disclosure: 'minimal',
@@ -132,6 +132,8 @@ class ConversationDriver {
132
132
  // If provided by caller, this keeps tool allowlists variable per token/profile.
133
133
  this.capabilities = Array.isArray(options.capabilities) ? options.capabilities : [];
134
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 : [];
135
137
  this.summarizer = options.summarizer || null;
136
138
  this.ownerContext = options.ownerContext || {};
137
139
  this.claudeMode = options.runtime?.mode === 'claude';
@@ -229,8 +231,11 @@ class ConversationDriver {
229
231
  // Try runtime.summarize if available (OpenClaw path)
230
232
  if (typeof runtime.summarize === 'function') {
231
233
  try {
234
+ const summarySessionId = this.lastConversationId
235
+ ? `a2a-${this.lastConversationId}`
236
+ : `summary-${Date.now()}`;
232
237
  return await runtime.summarize({
233
- sessionId: `summary-${Date.now()}`,
238
+ sessionId: summarySessionId,
234
239
  prompt,
235
240
  messages,
236
241
  callerInfo: { name: agentContext.name, owner: agentContext.owner },
@@ -414,7 +419,11 @@ class ConversationDriver {
414
419
  roleContext: 'You initiated this call.',
415
420
  capabilities: this.capabilities,
416
421
  allowedTopics: this.allowedTopics,
417
- allowed_topics: 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
418
427
  };
419
428
  if (this.claudeMode) {
420
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