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.
- 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/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 +100 -27
- package/src/lib/config.js +11 -0
- package/src/lib/conversation-driver.js +11 -2
- package/src/lib/disclosure.js +89 -13
- package/src/lib/runtime-adapter.js +42 -15
- package/src/lib/tokens.js +18 -0
- package/src/routes/a2a.js +4 -0
- package/src/routes/dashboard.js +9 -1
- package/src/server.js +42 -2
|
@@ -44,7 +44,10 @@ pub fn start_server() -> StartResult {
|
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
let port = crate::discovery::
|
|
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
|
@@ -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 Grep 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
|
-
*
|
|
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: ['
|
|
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
|
|
484
|
-
const allowedToolsArg = buildClaudeToolArg(
|
|
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:
|
|
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
|
|
561
|
-
const allowedToolsArg = buildClaudeToolArg(
|
|
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:
|
|
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:
|
|
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;
|
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
|