a2acalling 0.6.52 → 0.6.54

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.
@@ -1,56 +1,62 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // ============================================================================
4
- // npm postinstall hook — silent server setup
4
+ // npm postinstall hook — skill installation + silent server setup
5
5
  //
6
- // OBJECTIVE: When `npm install -g a2acalling` runs, this script silently
7
- // starts the A2A server and saves the initial config. No output is produced
8
- // because npm v7+ captures all lifecycle script stdio.
6
+ // TWO MODES:
9
7
  //
10
- // The VERBOSE onboarding (disclosure prompt, workspace scan instructions)
11
- // happens when the agent runs any `a2a` command enforceOnboarding detects
12
- // incomplete setup and runs the full quickstart flow inline with direct stdio.
8
+ // 1. Global install (npm install -g a2acalling):
9
+ // - Silently runs quickstart to start the server and save config
10
+ // - Installs Claude Code skills into INIT_CWD (if available)
11
+ // - Prints getting-started summary
13
12
  //
14
- // This split exists because:
15
- // - npm postinstall: stdio is piped/captured, output is invisible to agents
16
- // - `a2a <command>`: agent runs directly, stdio is clean, output is visible
13
+ // 2. Local install (npm install a2acalling):
14
+ // - Installs Claude Code skills (.claude/commands/) into the project
15
+ // - Prints getting-started summary so the assisting agent has context
16
+ // - Does NOT start the server (local installs are usually for development)
17
17
  //
18
- // So postinstall handles the parts that don't need output (server start),
19
- // and the a2a binary handles the parts that DO need output (disclosure prompt).
18
+ // The verbose onboarding (disclosure prompt, workspace scan) happens when the
19
+ // agent runs `a2a quickstart` enforceOnboarding detects incomplete setup
20
+ // and runs the full flow inline with direct stdio.
20
21
  // ============================================================================
21
22
 
22
23
  if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) process.exit(0);
23
24
  if (process.env.DOCKER) process.exit(0);
24
- if (process.env.npm_config_global !== 'true') process.exit(0);
25
25
 
26
26
  const path = require('path');
27
+ const os = require('os');
27
28
  const { spawnSync } = require('child_process');
28
29
 
30
+ const isGlobal = process.env.npm_config_global === 'true';
29
31
  const initCwd = process.env.INIT_CWD || process.env.HOME || process.cwd();
30
32
  const cliPath = path.join(__dirname, '..', 'bin', 'cli.js');
31
33
 
32
- // Run quickstart silently starts server, saves config, detects hostname.
33
- // All prompts auto-accept defaults (stdin is piped with no input).
34
- // Output is captured (not shown) because npm would swallow it anyway.
35
- const result = spawnSync(process.execPath, [cliPath, 'quickstart'], {
36
- stdio: ['pipe', 'pipe', 'pipe'],
37
- cwd: initCwd,
38
- env: {
39
- ...process.env,
40
- A2A_WORKSPACE: process.env.A2A_WORKSPACE || initCwd
41
- }
42
- });
34
+ // ── Global install: silently run quickstart ──────────────────────────────
35
+ if (isGlobal) {
36
+ const result = spawnSync(process.execPath, [cliPath, 'quickstart'], {
37
+ stdio: ['pipe', 'pipe', 'pipe'],
38
+ cwd: initCwd,
39
+ env: {
40
+ ...process.env,
41
+ A2A_WORKSPACE: process.env.A2A_WORKSPACE || initCwd
42
+ }
43
+ });
43
44
 
44
- if (result.error) {
45
- // Don't fail the install — the agent will get onboarding when it runs `a2a`.
46
- installSkillFiles();
47
- process.exit(0);
45
+ if (result.error) {
46
+ // Don't fail the install — the agent will get onboarding when it runs `a2a`.
47
+ }
48
48
  }
49
49
 
50
+ // ── Install skill files (both global and local) ─────────────────────────
50
51
  installSkillFiles();
51
- process.exit(result.status || 0);
52
52
 
53
- // Best-effort: install Claude Code + Codex skills into the workspace
53
+ // ── Print getting-started summary ───────────────────────────────────────
54
+ printGettingStarted();
55
+
56
+ process.exit(0);
57
+
58
+ // ── Helpers ─────────────────────────────────────────────────────────────
59
+
54
60
  function installSkillFiles() {
55
61
  try {
56
62
  const { installSkills } = require('./install-skills');
@@ -59,3 +65,120 @@ function installSkillFiles() {
59
65
  // Silent — skills can be installed later with `a2a skills`
60
66
  }
61
67
  }
68
+
69
+ function printGettingStarted() {
70
+ const isMac = os.platform() === 'darwin';
71
+ const pkg = require('../package.json');
72
+
73
+ const lines = [
74
+ '',
75
+ '╔══════════════════════════════════════════════════════════════╗',
76
+ '║ A2A Calling — Installed ║',
77
+ '╚══════════════════════════════════════════════════════════════╝',
78
+ '',
79
+ ` Version: ${pkg.version}`,
80
+ '',
81
+ ' ── Getting Started ──────────────────────────────────────────',
82
+ '',
83
+ ' 1. Run onboarding (REQUIRED first step):',
84
+ '',
85
+ ' a2a quickstart',
86
+ '',
87
+ ' This starts the A2A server, detects your hostname,',
88
+ ' and prompts you to configure what your agent shares.',
89
+ '',
90
+ ' 2. Create an invite to share with other agents:',
91
+ '',
92
+ ' a2a create --name "YourAgent" --tier public --expires 7d',
93
+ '',
94
+ ' 3. Add a contact and call them:',
95
+ '',
96
+ ' a2a add "a2a://host/fed_xxx" "AgentName"',
97
+ ' a2a call "AgentName" "Hello!"',
98
+ '',
99
+ ];
100
+
101
+ if (isMac) {
102
+ lines.push(
103
+ ' ── Native macOS App ─────────────────────────────────────────',
104
+ '',
105
+ ' A native Callbook app is available for macOS:',
106
+ '',
107
+ ' a2a app install',
108
+ '',
109
+ ' Installs to ~/Applications/A2A Callbook.app',
110
+ ' (Downloads pre-built binary from GitHub releases)',
111
+ '',
112
+ );
113
+ }
114
+
115
+ lines.push(
116
+ ' ── Full CLI Reference ───────────────────────────────────────',
117
+ '',
118
+ ' Onboarding & Setup:',
119
+ ' a2a quickstart First-time setup (port, hostname, disclosure)',
120
+ ' a2a quickstart --force Re-run onboarding from scratch',
121
+ ' a2a setup Auto setup (gateway-aware dashboard install)',
122
+ ' a2a status <url> Check A2A agent status',
123
+ ' a2a version Show installed version',
124
+ '',
125
+ ' Tokens & Invites:',
126
+ ' a2a create [options] Create an invite token',
127
+ ' --name, -n NAME Token label',
128
+ ' --tier, -p TIER public | friends | family',
129
+ ' --expires DURATION 1h | 1d | 7d | 30d | never',
130
+ ' a2a list List active tokens',
131
+ ' a2a revoke <id> Revoke a token',
132
+ '',
133
+ ' Contacts & Calling:',
134
+ ' a2a add <url> [name] Add a contact from invite URL',
135
+ ' a2a contacts List all contacts',
136
+ ' a2a call <contact> <msg> Call a contact (multi-turn)',
137
+ ' --single One-shot call (no back-and-forth)',
138
+ ' a2a ping <url> Check if agent is reachable',
139
+ '',
140
+ ' Dashboard & GUI:',
141
+ ' a2a gui Open dashboard in browser',
142
+ ' a2a gui --tab logs Open specific tab',
143
+ '',
144
+ ' Server:',
145
+ ' a2a server --port 3001 Start server manually',
146
+ ' a2a update Update to latest version',
147
+ ' a2a uninstall Stop server and remove config',
148
+ '',
149
+ );
150
+
151
+ if (isMac) {
152
+ lines.push(
153
+ ' Native App (macOS):',
154
+ ' a2a app status Check native app installation',
155
+ ' a2a app install Install/update from GitHub releases',
156
+ ' a2a app uninstall Remove from ~/Applications',
157
+ '',
158
+ );
159
+ }
160
+
161
+ lines.push(
162
+ ' Skills:',
163
+ ' a2a skills Install Claude Code + Codex skills',
164
+ ' a2a skills --force Overwrite existing skill files',
165
+ '',
166
+ ' ── Claude Code Skills Installed ────────────────────────────',
167
+ '',
168
+ ' The following slash commands are now available:',
169
+ ' /a2a-setup — Run onboarding or reset configuration',
170
+ ' /a2a-call — Call another A2A agent',
171
+ ' /a2a-invite — Create and share an invite token',
172
+ ' /a2a-contacts — List and manage contacts',
173
+ ' /a2a-status — Check server and agent health',
174
+ '',
175
+ '══════════════════════════════════════════════════════════════',
176
+ '',
177
+ );
178
+
179
+ // Print to stderr — npm v7+ captures stdout from lifecycle scripts,
180
+ // but stderr is still visible in many agent contexts
181
+ console.error(lines.join('\n'));
182
+ // Also print to stdout for contexts where stderr is filtered
183
+ console.log(lines.join('\n'));
184
+ }
@@ -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;