a2acalling 0.6.51 → 0.6.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/health.rs +2 -3
- package/native/macos/src-tauri/src/notifications.rs +1 -1
- 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 +302 -92
- package/src/lib/config.js +11 -0
- package/src/lib/conversation-driver.js +18 -2
- package/src/lib/disclosure.js +89 -13
- package/src/lib/runtime-adapter.js +76 -20
- package/src/lib/tokens.js +18 -0
- package/src/routes/a2a.js +7 -0
- package/src/routes/dashboard.js +9 -1
- package/src/server.js +44 -2
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
|
|
2
|
-
use std::sync::Arc;
|
|
3
2
|
use std::time::Duration;
|
|
4
3
|
use tauri::{Emitter, Manager};
|
|
5
4
|
|
|
@@ -21,8 +20,8 @@ pub fn set_connected(port: u16) {
|
|
|
21
20
|
|
|
22
21
|
/// Start background health check loop — emits "server-status" events
|
|
23
22
|
pub fn start_health_monitor(app: tauri::AppHandle) {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
tauri::async_runtime::spawn(async move {
|
|
24
|
+
let handle = app;
|
|
26
25
|
loop {
|
|
27
26
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
|
28
27
|
|
|
@@ -75,7 +75,7 @@ fn process_dashboard_event(app: &tauri::AppHandle, raw: &str) {
|
|
|
75
75
|
|
|
76
76
|
/// Connect to server-driven dashboard SSE and map events to native notifications.
|
|
77
77
|
pub fn start_event_stream_listener(app: tauri::AppHandle) {
|
|
78
|
-
|
|
78
|
+
tauri::async_runtime::spawn(async move {
|
|
79
79
|
// Wait for initial discovery attempt.
|
|
80
80
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
81
81
|
|
|
@@ -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">
|
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
* Spawns `claude` CLI processes for real LLM-powered A2A conversations
|
|
5
5
|
* as an alternative to OpenClaw for A2A conversations.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Design decision (A2A-29):
|
|
8
|
+
* We intentionally run Claude turns in stateless one-shot mode instead of `--resume`.
|
|
9
|
+
* In production we observed intermittent hangs during nested Claude startup/restore.
|
|
10
|
+
* Stateless calls cost more tokens but are operationally safer under load.
|
|
11
|
+
*
|
|
12
|
+
* Permissioning is still enforced:
|
|
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.
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
const { execSync, spawn } = require('child_process');
|
|
@@ -14,6 +21,43 @@ const { HARD_FALLBACK_TURN_TIMEOUT_MS } = require('./turn-timeout');
|
|
|
14
21
|
const logger = createLogger({ component: 'a2a.claude-subagent' });
|
|
15
22
|
|
|
16
23
|
const A2A_RESPONSE_REGEX = /<a2a_response>\s*([\s\S]*?)\s*<\/a2a_response>/i;
|
|
24
|
+
const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-5-20250929';
|
|
25
|
+
const CLAUDE_TOOL_UNIVERSE = ['Bash', 'Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
|
|
26
|
+
const LEGACY_DEFAULT_TOOLS = ['Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
|
|
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
|
+
}
|
|
17
61
|
|
|
18
62
|
/**
|
|
19
63
|
* Check if `claude` CLI is available in PATH.
|
|
@@ -40,6 +84,7 @@ function isClaudeAvailable() {
|
|
|
40
84
|
* @param {string} config.tierObjectives - formatted objectives string
|
|
41
85
|
* @param {string} config.doNotDiscuss - formatted do_not_discuss string
|
|
42
86
|
* @param {string} config.neverDisclose - formatted never_disclose string
|
|
87
|
+
* @param {string[]} [config.allowedTools] - Effective tool allowlist for this call
|
|
43
88
|
* @param {string} config.personalityNotes
|
|
44
89
|
* @param {string} config.roleContext
|
|
45
90
|
* @returns {string}
|
|
@@ -55,10 +100,17 @@ function buildSubagentSystemPrompt(config) {
|
|
|
55
100
|
tierObjectives = ' (none specified)',
|
|
56
101
|
doNotDiscuss = ' (none specified)',
|
|
57
102
|
neverDisclose = ' (none specified)',
|
|
103
|
+
allowedTools = [],
|
|
58
104
|
personalityNotes = '',
|
|
59
105
|
roleContext = ''
|
|
60
106
|
} = config;
|
|
61
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
|
+
|
|
62
114
|
return `You are ${agentName}, the personal AI agent for ${ownerName}.
|
|
63
115
|
You are on a live A2A (agent-to-agent) call with ${otherAgentName}, who represents ${otherOwnerName}. ${roleContext}
|
|
64
116
|
|
|
@@ -99,6 +151,19 @@ ${doNotDiscuss}
|
|
|
99
151
|
NEVER disclose:
|
|
100
152
|
${neverDisclose}
|
|
101
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
|
+
|
|
102
167
|
== BEHAVIORAL MANDATE ==
|
|
103
168
|
|
|
104
169
|
You operate in three concurrent modes:
|
|
@@ -219,13 +284,11 @@ function parseSubagentResponse(resultText) {
|
|
|
219
284
|
*/
|
|
220
285
|
function spawnClaude(args, timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS) {
|
|
221
286
|
return new Promise((resolve, reject) => {
|
|
287
|
+
const spawnEnv = { ...process.env, FORCE_COLOR: '0' };
|
|
288
|
+
delete spawnEnv.CLAUDECODE;
|
|
222
289
|
const proc = spawn('claude', args, {
|
|
223
|
-
stdio: ['
|
|
224
|
-
env:
|
|
225
|
-
...process.env,
|
|
226
|
-
FORCE_COLOR: '0',
|
|
227
|
-
CLAUDECODE: '' // Unset to allow nested invocation
|
|
228
|
-
}
|
|
290
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
291
|
+
env: spawnEnv
|
|
229
292
|
});
|
|
230
293
|
|
|
231
294
|
let stdout = '';
|
|
@@ -289,11 +352,158 @@ function extractResultFromJson(stdout) {
|
|
|
289
352
|
}
|
|
290
353
|
}
|
|
291
354
|
|
|
355
|
+
function normalizePermissionList(values) {
|
|
356
|
+
if (!Array.isArray(values)) return [];
|
|
357
|
+
return values
|
|
358
|
+
.map(v => String(v || '').trim().toLowerCase())
|
|
359
|
+
.filter(Boolean);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function hasPermissionMatch(values, key) {
|
|
363
|
+
if (!key) return false;
|
|
364
|
+
return values.some(value => value === key || value.startsWith(`${key}.`));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function deriveClaudeToolsFromPermissionSignals({ capabilities = [], allowedTopics = [] } = {}) {
|
|
368
|
+
const normalizedCaps = normalizePermissionList(capabilities);
|
|
369
|
+
const normalizedTopics = normalizePermissionList(allowedTopics);
|
|
370
|
+
const hasPermissionContext = normalizedCaps.length > 0 || normalizedTopics.length > 0;
|
|
371
|
+
|
|
372
|
+
if (!hasPermissionContext) {
|
|
373
|
+
return [...LEGACY_DEFAULT_TOOLS];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const hasContextRead = hasPermissionMatch(normalizedCaps, 'context-read')
|
|
377
|
+
|| normalizedTopics.includes('chat');
|
|
378
|
+
const hasSearch = hasPermissionMatch(normalizedCaps, 'search')
|
|
379
|
+
|| normalizedTopics.includes('search');
|
|
380
|
+
const hasToolsRead = hasPermissionMatch(normalizedCaps, 'tools')
|
|
381
|
+
|| hasPermissionMatch(normalizedCaps, 'tools-read')
|
|
382
|
+
|| normalizedTopics.includes('tools');
|
|
383
|
+
const hasToolsWrite = hasPermissionMatch(normalizedCaps, 'tools-write')
|
|
384
|
+
|| hasPermissionMatch(normalizedCaps, 'tools.write')
|
|
385
|
+
|| normalizedTopics.includes('tools-write')
|
|
386
|
+
|| normalizedTopics.includes('tools.write');
|
|
387
|
+
|
|
388
|
+
const tools = [];
|
|
389
|
+
|
|
390
|
+
// Keep read-only introspection available for context-aware tiers.
|
|
391
|
+
if (hasContextRead || hasSearch || hasToolsRead || hasToolsWrite) {
|
|
392
|
+
tools.push('Read', 'Grep', 'Glob');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Web tools are explicitly tied to search-style permissions.
|
|
396
|
+
if (hasSearch) {
|
|
397
|
+
tools.push('WebSearch', 'WebFetch');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Shell access is gated behind tool permissions, with explicit writable opt-in.
|
|
401
|
+
if (hasToolsWrite) {
|
|
402
|
+
tools.unshift('Bash');
|
|
403
|
+
} else if (hasToolsRead) {
|
|
404
|
+
tools.unshift('Bash(readonly)');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Fail closed to read-only file inspection if metadata is custom/unknown.
|
|
408
|
+
if (tools.length === 0) {
|
|
409
|
+
return ['Read', 'Grep', 'Glob'];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return tools;
|
|
413
|
+
}
|
|
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
|
+
|
|
438
|
+
function buildClaudeToolArg(allowedTools) {
|
|
439
|
+
return Array.isArray(allowedTools) ? allowedTools.join(' ').trim() : '';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function parseSummaryPayload(resultText) {
|
|
443
|
+
const text = String(resultText || '').trim();
|
|
444
|
+
if (!text) return null;
|
|
445
|
+
|
|
446
|
+
// Backwards-compatible: older prompts wrapped JSON in <a2a_response>.
|
|
447
|
+
const tagged = text.match(A2A_RESPONSE_REGEX);
|
|
448
|
+
if (tagged && tagged[1]) {
|
|
449
|
+
try {
|
|
450
|
+
return JSON.parse(tagged[1].trim());
|
|
451
|
+
} catch (err) {
|
|
452
|
+
logger.warn('Failed to parse tagged summary JSON', {
|
|
453
|
+
event: 'subagent_summary_tag_parse_failed',
|
|
454
|
+
error: err
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Preferred path for unified summary prompt: direct JSON object.
|
|
460
|
+
try {
|
|
461
|
+
const parsed = JSON.parse(text);
|
|
462
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
463
|
+
return parsed;
|
|
464
|
+
}
|
|
465
|
+
} catch (err) {
|
|
466
|
+
logger.debug('Summary result is not direct JSON; falling back to plain text summary', {
|
|
467
|
+
event: 'subagent_summary_raw_fallback',
|
|
468
|
+
data: { output_length: text.length }
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function summarizeFromPayload(payload, fallbackText) {
|
|
476
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Native A2A summary payload shape.
|
|
481
|
+
if (typeof payload.summary === 'string' || typeof payload.ownerSummary === 'string') {
|
|
482
|
+
return {
|
|
483
|
+
summary: payload.summary || payload.message || fallbackText || '',
|
|
484
|
+
ownerSummary: payload.ownerSummary || payload.summary || payload.message || fallbackText || '',
|
|
485
|
+
actionItems: Array.isArray(payload.actionItems) ? payload.actionItems : [],
|
|
486
|
+
flags: Array.isArray(payload.flags) ? payload.flags : []
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Unified summary schema shape (headline/assessment/nextSteps).
|
|
491
|
+
if (typeof payload.headline === 'string') {
|
|
492
|
+
return {
|
|
493
|
+
summary: payload.headline,
|
|
494
|
+
ownerSummary: typeof payload.assessment === 'string' ? payload.assessment : payload.headline,
|
|
495
|
+
actionItems: Array.isArray(payload.nextSteps) ? payload.nextSteps : [],
|
|
496
|
+
flags: []
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
292
503
|
/**
|
|
293
504
|
* Run a single turn of the Claude subagent.
|
|
294
505
|
*
|
|
295
506
|
* @param {Object} options
|
|
296
|
-
* @param {string} options.sessionId - Conversation session ID (used for --resume on turn 2+)
|
|
297
507
|
* @param {string} options.systemPrompt - System prompt (used on turn 1 only)
|
|
298
508
|
* @param {string} options.turnMessage - The inbound message from the remote agent
|
|
299
509
|
* @param {number} options.turn - Current turn number (1-based)
|
|
@@ -303,12 +513,15 @@ function extractResultFromJson(stdout) {
|
|
|
303
513
|
* @param {Array} options.activeThreads - Active conversation threads
|
|
304
514
|
* @param {Array} options.candidateCollaborations - Candidate collaboration ideas
|
|
305
515
|
* @param {boolean} options.closeSignal - Whether close has been signaled
|
|
516
|
+
* @param {Array<string>} [options.capabilities] - Token capabilities (permission source of truth)
|
|
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)
|
|
519
|
+
* @param {function} [options.spawnFn] - Injectable process runner for tests
|
|
306
520
|
* @param {number} [options.timeoutMs=300000] - Timeout in milliseconds
|
|
307
|
-
* @returns {Promise<{ message: string, statePatch: object|null, flags: array
|
|
521
|
+
* @returns {Promise<{ message: string, statePatch: object|null, flags: array }>}
|
|
308
522
|
*/
|
|
309
523
|
async function runClaudeTurn(options) {
|
|
310
524
|
const {
|
|
311
|
-
sessionId,
|
|
312
525
|
systemPrompt,
|
|
313
526
|
turnMessage,
|
|
314
527
|
turn = 1,
|
|
@@ -318,6 +531,10 @@ async function runClaudeTurn(options) {
|
|
|
318
531
|
activeThreads = [],
|
|
319
532
|
candidateCollaborations = [],
|
|
320
533
|
closeSignal = false,
|
|
534
|
+
capabilities = [],
|
|
535
|
+
allowedTopics = [],
|
|
536
|
+
allowedTools = [],
|
|
537
|
+
spawnFn = spawnClaude,
|
|
321
538
|
timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
322
539
|
} = options;
|
|
323
540
|
|
|
@@ -333,29 +550,21 @@ async function runClaudeTurn(options) {
|
|
|
333
550
|
});
|
|
334
551
|
|
|
335
552
|
const startAt = Date.now();
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
} else {
|
|
350
|
-
// Subsequent turns: resume existing session
|
|
351
|
-
args = [
|
|
352
|
-
'-p',
|
|
353
|
-
'--output-format', 'json',
|
|
354
|
-
'--resume', sessionId,
|
|
355
|
-
'--allowedTools', allowedTools,
|
|
356
|
-
turnPrompt
|
|
357
|
-
];
|
|
553
|
+
const effectiveAllowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics, allowedTools });
|
|
554
|
+
const allowedToolsArg = buildClaudeToolArg(effectiveAllowedTools);
|
|
555
|
+
const args = [
|
|
556
|
+
'-p',
|
|
557
|
+
'--output-format', 'json',
|
|
558
|
+
'--system-prompt', systemPrompt,
|
|
559
|
+
'--model', DEFAULT_CLAUDE_MODEL
|
|
560
|
+
];
|
|
561
|
+
|
|
562
|
+
// We always provide --allowedTools explicitly so token permissioning stays
|
|
563
|
+
// enforced in Claude mode even after moving to stateless turns.
|
|
564
|
+
if (allowedToolsArg) {
|
|
565
|
+
args.push('--allowedTools', allowedToolsArg);
|
|
358
566
|
}
|
|
567
|
+
args.push('--', turnPrompt);
|
|
359
568
|
|
|
360
569
|
logger.debug('Spawning Claude subagent turn', {
|
|
361
570
|
event: 'subagent_turn_start',
|
|
@@ -363,13 +572,14 @@ async function runClaudeTurn(options) {
|
|
|
363
572
|
turn,
|
|
364
573
|
max_turns: maxTurns,
|
|
365
574
|
phase,
|
|
366
|
-
|
|
575
|
+
is_stateless: true,
|
|
576
|
+
allowed_tools: effectiveAllowedTools,
|
|
367
577
|
timeout_ms: timeoutMs
|
|
368
578
|
}
|
|
369
579
|
});
|
|
370
580
|
|
|
371
|
-
const { stdout } = await
|
|
372
|
-
const { result
|
|
581
|
+
const { stdout } = await spawnFn(args, timeoutMs);
|
|
582
|
+
const { result } = extractResultFromJson(stdout);
|
|
373
583
|
const parsed = parseSubagentResponse(result);
|
|
374
584
|
|
|
375
585
|
logger.debug('Claude subagent turn completed', {
|
|
@@ -379,91 +589,90 @@ async function runClaudeTurn(options) {
|
|
|
379
589
|
duration_ms: Date.now() - startAt,
|
|
380
590
|
message_length: parsed.message.length,
|
|
381
591
|
has_state_patch: Boolean(parsed.statePatch),
|
|
382
|
-
flag_count: parsed.flags.length
|
|
383
|
-
session_id: newSessionId || sessionId
|
|
592
|
+
flag_count: parsed.flags.length
|
|
384
593
|
}
|
|
385
594
|
});
|
|
386
595
|
|
|
387
596
|
return {
|
|
388
597
|
message: parsed.message,
|
|
389
598
|
statePatch: parsed.statePatch,
|
|
390
|
-
flags: parsed.flags
|
|
391
|
-
sessionId: newSessionId || sessionId
|
|
599
|
+
flags: parsed.flags
|
|
392
600
|
};
|
|
393
601
|
}
|
|
394
602
|
|
|
395
603
|
/**
|
|
396
|
-
* Run a summary turn
|
|
604
|
+
* Run a summary turn in stateless Claude mode.
|
|
397
605
|
*
|
|
398
|
-
* @param {
|
|
399
|
-
* @param {string}
|
|
400
|
-
* @param {
|
|
606
|
+
* @param {Object} options
|
|
607
|
+
* @param {string} options.prompt - Unified summary prompt
|
|
608
|
+
* @param {string} [options.reason] - Why the conversation is ending
|
|
609
|
+
* @param {Array<string>} [options.capabilities] - Token capabilities for summary turn tooling
|
|
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
|
|
612
|
+
* @param {function} [options.spawnFn] - Injectable process runner for tests
|
|
613
|
+
* @param {number} [options.timeoutMs=300000] - Timeout in milliseconds
|
|
401
614
|
* @returns {Promise<{ summary: string, ownerSummary: string, actionItems: array, flags: array }>}
|
|
402
615
|
*/
|
|
403
|
-
async function runClaudeSummary(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
616
|
+
async function runClaudeSummary(options = {}) {
|
|
617
|
+
const {
|
|
618
|
+
prompt,
|
|
619
|
+
reason,
|
|
620
|
+
capabilities = [],
|
|
621
|
+
allowedTopics = [],
|
|
622
|
+
allowedTools = [],
|
|
623
|
+
spawnFn = spawnClaude,
|
|
624
|
+
timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
625
|
+
} = options;
|
|
407
626
|
|
|
408
|
-
const summaryPrompt =
|
|
627
|
+
const summaryPrompt = String(prompt || '').trim();
|
|
628
|
+
if (!summaryPrompt) {
|
|
629
|
+
throw new Error('Cannot summarize without a prompt');
|
|
630
|
+
}
|
|
409
631
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
<a2a_response>
|
|
413
|
-
{
|
|
414
|
-
"message": "Brief 1-2 sentence summary of the conversation.",
|
|
415
|
-
"statePatch": {"phase": "close", "closeSignal": true},
|
|
416
|
-
"flags": [],
|
|
417
|
-
"summary": "Detailed summary for the conversation record.",
|
|
418
|
-
"ownerSummary": "Summary written for the owner highlighting key findings and opportunities.",
|
|
419
|
-
"actionItems": ["Specific follow-up item 1", "Specific follow-up item 2"]
|
|
420
|
-
}
|
|
421
|
-
</a2a_response>`;
|
|
632
|
+
const effectiveAllowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics, allowedTools });
|
|
633
|
+
const allowedToolsArg = buildClaudeToolArg(effectiveAllowedTools);
|
|
422
634
|
|
|
423
635
|
const args = [
|
|
424
636
|
'-p',
|
|
425
637
|
'--output-format', 'json',
|
|
426
|
-
'--
|
|
427
|
-
summaryPrompt
|
|
638
|
+
'--model', DEFAULT_CLAUDE_MODEL
|
|
428
639
|
];
|
|
429
640
|
|
|
641
|
+
if (allowedToolsArg) {
|
|
642
|
+
args.push('--allowedTools', allowedToolsArg);
|
|
643
|
+
}
|
|
644
|
+
args.push(
|
|
645
|
+
'--append-system-prompt',
|
|
646
|
+
`Conversation summary mode. Reason: ${reason || 'conversation ended'}. Return only structured summary JSON.`,
|
|
647
|
+
'--',
|
|
648
|
+
summaryPrompt
|
|
649
|
+
);
|
|
650
|
+
|
|
430
651
|
const startAt = Date.now();
|
|
431
652
|
|
|
432
653
|
logger.debug('Spawning Claude summary', {
|
|
433
654
|
event: 'subagent_summary_start',
|
|
434
|
-
data: {
|
|
655
|
+
data: {
|
|
656
|
+
reason: reason || 'conversation ended',
|
|
657
|
+
allowed_tools: effectiveAllowedTools
|
|
658
|
+
}
|
|
435
659
|
});
|
|
436
660
|
|
|
437
|
-
const { stdout } = await
|
|
661
|
+
const { stdout } = await spawnFn(args, timeoutMs);
|
|
438
662
|
const { result } = extractResultFromJson(stdout);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
return {
|
|
456
|
-
summary: parsed.summary || parsed.message || result.replace(A2A_RESPONSE_REGEX, '').trim(),
|
|
457
|
-
ownerSummary: parsed.ownerSummary || parsed.summary || parsed.message || '',
|
|
458
|
-
actionItems: Array.isArray(parsed.actionItems) ? parsed.actionItems : [],
|
|
459
|
-
flags: Array.isArray(parsed.flags) ? parsed.flags : []
|
|
460
|
-
};
|
|
461
|
-
} catch (err) {
|
|
462
|
-
logger.warn('Failed to parse summary JSON', {
|
|
463
|
-
event: 'subagent_summary_parse_failed',
|
|
464
|
-
error: err
|
|
465
|
-
});
|
|
466
|
-
}
|
|
663
|
+
const summaryPayload = parseSummaryPayload(result);
|
|
664
|
+
const parsedSummary = summarizeFromPayload(summaryPayload, result.trim());
|
|
665
|
+
|
|
666
|
+
if (parsedSummary) {
|
|
667
|
+
logger.debug('Claude summary completed', {
|
|
668
|
+
event: 'subagent_summary_complete',
|
|
669
|
+
data: {
|
|
670
|
+
duration_ms: Date.now() - startAt,
|
|
671
|
+
has_summary: Boolean(parsedSummary.summary),
|
|
672
|
+
action_item_count: parsedSummary.actionItems.length
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
return parsedSummary;
|
|
467
676
|
}
|
|
468
677
|
|
|
469
678
|
// Fallback: use raw text as summary
|
|
@@ -480,6 +689,7 @@ module.exports = {
|
|
|
480
689
|
isClaudeAvailable,
|
|
481
690
|
buildSubagentSystemPrompt,
|
|
482
691
|
buildTurnPrompt,
|
|
692
|
+
resolveClaudeAllowedTools,
|
|
483
693
|
runClaudeTurn,
|
|
484
694
|
runClaudeSummary,
|
|
485
695
|
parseSubagentResponse
|
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',
|