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.
- package/.claude/commands/a2a-call.md +26 -0
- package/.claude/commands/a2a-contacts.md +31 -0
- package/.claude/commands/a2a-invite.md +33 -0
- package/.claude/commands/a2a-setup.md +30 -0
- package/.claude/commands/a2a-status.md +24 -0
- package/CLAUDE-INSTALL.md +154 -0
- 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/scripts/install-skills.js +40 -5
- package/scripts/postinstall.js +153 -30
- 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
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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
|
-
//
|
|
19
|
-
//
|
|
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
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
//
|
|
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 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;
|