a2acalling 0.6.51 → 0.6.52
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/native/macos/src-tauri/src/health.rs +2 -3
- package/native/macos/src-tauri/src/notifications.rs +1 -1
- package/package.json +1 -1
- package/src/lib/claude-subagent.js +222 -85
- package/src/lib/conversation-driver.js +8 -1
- package/src/lib/runtime-adapter.js +46 -17
- package/src/routes/a2a.js +3 -0
- package/src/server.js +2 -0
|
@@ -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
|
|
package/package.json
CHANGED
|
@@ -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
|
+
* We do not hardcode one universal allowlist anymore.
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
const { execSync, spawn } = require('child_process');
|
|
@@ -14,6 +21,8 @@ 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 LEGACY_DEFAULT_TOOLS = ['Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
28
|
* Check if `claude` CLI is available in PATH.
|
|
@@ -289,11 +298,144 @@ function extractResultFromJson(stdout) {
|
|
|
289
298
|
}
|
|
290
299
|
}
|
|
291
300
|
|
|
301
|
+
function normalizePermissionList(values) {
|
|
302
|
+
if (!Array.isArray(values)) return [];
|
|
303
|
+
return values
|
|
304
|
+
.map(v => String(v || '').trim().toLowerCase())
|
|
305
|
+
.filter(Boolean);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasPermissionMatch(values, key) {
|
|
309
|
+
if (!key) return false;
|
|
310
|
+
return values.some(value => value === key || value.startsWith(`${key}.`));
|
|
311
|
+
}
|
|
312
|
+
|
|
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 = [] } = {}) {
|
|
323
|
+
const normalizedCaps = normalizePermissionList(capabilities);
|
|
324
|
+
const normalizedTopics = normalizePermissionList(allowedTopics);
|
|
325
|
+
const hasPermissionContext = normalizedCaps.length > 0 || normalizedTopics.length > 0;
|
|
326
|
+
|
|
327
|
+
if (!hasPermissionContext) {
|
|
328
|
+
return [...LEGACY_DEFAULT_TOOLS];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const hasContextRead = hasPermissionMatch(normalizedCaps, 'context-read')
|
|
332
|
+
|| normalizedTopics.includes('chat');
|
|
333
|
+
const hasSearch = hasPermissionMatch(normalizedCaps, 'search')
|
|
334
|
+
|| normalizedTopics.includes('search');
|
|
335
|
+
const hasToolsRead = hasPermissionMatch(normalizedCaps, 'tools')
|
|
336
|
+
|| hasPermissionMatch(normalizedCaps, 'tools-read')
|
|
337
|
+
|| normalizedTopics.includes('tools');
|
|
338
|
+
const hasToolsWrite = hasPermissionMatch(normalizedCaps, 'tools-write')
|
|
339
|
+
|| hasPermissionMatch(normalizedCaps, 'tools.write')
|
|
340
|
+
|| normalizedTopics.includes('tools-write')
|
|
341
|
+
|| normalizedTopics.includes('tools.write');
|
|
342
|
+
|
|
343
|
+
const tools = [];
|
|
344
|
+
|
|
345
|
+
// Keep read-only introspection available for context-aware tiers.
|
|
346
|
+
if (hasContextRead || hasSearch || hasToolsRead || hasToolsWrite) {
|
|
347
|
+
tools.push('Read', 'Grep', 'Glob');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Web tools are explicitly tied to search-style permissions.
|
|
351
|
+
if (hasSearch) {
|
|
352
|
+
tools.push('WebSearch', 'WebFetch');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Shell access is gated behind tool permissions, with explicit writable opt-in.
|
|
356
|
+
if (hasToolsWrite) {
|
|
357
|
+
tools.unshift('Bash');
|
|
358
|
+
} else if (hasToolsRead) {
|
|
359
|
+
tools.unshift('Bash(readonly)');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Fail closed to read-only file inspection if metadata is custom/unknown.
|
|
363
|
+
if (tools.length === 0) {
|
|
364
|
+
return ['Read', 'Grep', 'Glob'];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return tools;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildClaudeToolArg(allowedTools) {
|
|
371
|
+
return Array.isArray(allowedTools) ? allowedTools.join(' ').trim() : '';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parseSummaryPayload(resultText) {
|
|
375
|
+
const text = String(resultText || '').trim();
|
|
376
|
+
if (!text) return null;
|
|
377
|
+
|
|
378
|
+
// Backwards-compatible: older prompts wrapped JSON in <a2a_response>.
|
|
379
|
+
const tagged = text.match(A2A_RESPONSE_REGEX);
|
|
380
|
+
if (tagged && tagged[1]) {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse(tagged[1].trim());
|
|
383
|
+
} catch (err) {
|
|
384
|
+
logger.warn('Failed to parse tagged summary JSON', {
|
|
385
|
+
event: 'subagent_summary_tag_parse_failed',
|
|
386
|
+
error: err
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Preferred path for unified summary prompt: direct JSON object.
|
|
392
|
+
try {
|
|
393
|
+
const parsed = JSON.parse(text);
|
|
394
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
395
|
+
return parsed;
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
logger.debug('Summary result is not direct JSON; falling back to plain text summary', {
|
|
399
|
+
event: 'subagent_summary_raw_fallback',
|
|
400
|
+
data: { output_length: text.length }
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function summarizeFromPayload(payload, fallbackText) {
|
|
408
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Native A2A summary payload shape.
|
|
413
|
+
if (typeof payload.summary === 'string' || typeof payload.ownerSummary === 'string') {
|
|
414
|
+
return {
|
|
415
|
+
summary: payload.summary || payload.message || fallbackText || '',
|
|
416
|
+
ownerSummary: payload.ownerSummary || payload.summary || payload.message || fallbackText || '',
|
|
417
|
+
actionItems: Array.isArray(payload.actionItems) ? payload.actionItems : [],
|
|
418
|
+
flags: Array.isArray(payload.flags) ? payload.flags : []
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Unified summary schema shape (headline/assessment/nextSteps).
|
|
423
|
+
if (typeof payload.headline === 'string') {
|
|
424
|
+
return {
|
|
425
|
+
summary: payload.headline,
|
|
426
|
+
ownerSummary: typeof payload.assessment === 'string' ? payload.assessment : payload.headline,
|
|
427
|
+
actionItems: Array.isArray(payload.nextSteps) ? payload.nextSteps : [],
|
|
428
|
+
flags: []
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
292
435
|
/**
|
|
293
436
|
* Run a single turn of the Claude subagent.
|
|
294
437
|
*
|
|
295
438
|
* @param {Object} options
|
|
296
|
-
* @param {string} options.sessionId - Conversation session ID (used for --resume on turn 2+)
|
|
297
439
|
* @param {string} options.systemPrompt - System prompt (used on turn 1 only)
|
|
298
440
|
* @param {string} options.turnMessage - The inbound message from the remote agent
|
|
299
441
|
* @param {number} options.turn - Current turn number (1-based)
|
|
@@ -303,12 +445,14 @@ function extractResultFromJson(stdout) {
|
|
|
303
445
|
* @param {Array} options.activeThreads - Active conversation threads
|
|
304
446
|
* @param {Array} options.candidateCollaborations - Candidate collaboration ideas
|
|
305
447
|
* @param {boolean} options.closeSignal - Whether close has been signaled
|
|
448
|
+
* @param {Array<string>} [options.capabilities] - Token capabilities (permission source of truth)
|
|
449
|
+
* @param {Array<string>} [options.allowedTopics] - Token allowed topics (permission source of truth)
|
|
450
|
+
* @param {function} [options.spawnFn] - Injectable process runner for tests
|
|
306
451
|
* @param {number} [options.timeoutMs=300000] - Timeout in milliseconds
|
|
307
|
-
* @returns {Promise<{ message: string, statePatch: object|null, flags: array
|
|
452
|
+
* @returns {Promise<{ message: string, statePatch: object|null, flags: array }>}
|
|
308
453
|
*/
|
|
309
454
|
async function runClaudeTurn(options) {
|
|
310
455
|
const {
|
|
311
|
-
sessionId,
|
|
312
456
|
systemPrompt,
|
|
313
457
|
turnMessage,
|
|
314
458
|
turn = 1,
|
|
@@ -318,6 +462,9 @@ async function runClaudeTurn(options) {
|
|
|
318
462
|
activeThreads = [],
|
|
319
463
|
candidateCollaborations = [],
|
|
320
464
|
closeSignal = false,
|
|
465
|
+
capabilities = [],
|
|
466
|
+
allowedTopics = [],
|
|
467
|
+
spawnFn = spawnClaude,
|
|
321
468
|
timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
322
469
|
} = options;
|
|
323
470
|
|
|
@@ -333,29 +480,21 @@ async function runClaudeTurn(options) {
|
|
|
333
480
|
});
|
|
334
481
|
|
|
335
482
|
const startAt = Date.now();
|
|
336
|
-
const allowedTools =
|
|
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
|
-
];
|
|
483
|
+
const allowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics });
|
|
484
|
+
const allowedToolsArg = buildClaudeToolArg(allowedTools);
|
|
485
|
+
const args = [
|
|
486
|
+
'-p',
|
|
487
|
+
'--output-format', 'json',
|
|
488
|
+
'--system-prompt', systemPrompt,
|
|
489
|
+
'--model', DEFAULT_CLAUDE_MODEL
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
// We always provide --allowedTools explicitly so token permissioning stays
|
|
493
|
+
// enforced in Claude mode even after moving to stateless turns.
|
|
494
|
+
if (allowedToolsArg) {
|
|
495
|
+
args.push('--allowedTools', allowedToolsArg);
|
|
358
496
|
}
|
|
497
|
+
args.push(turnPrompt);
|
|
359
498
|
|
|
360
499
|
logger.debug('Spawning Claude subagent turn', {
|
|
361
500
|
event: 'subagent_turn_start',
|
|
@@ -363,13 +502,14 @@ async function runClaudeTurn(options) {
|
|
|
363
502
|
turn,
|
|
364
503
|
max_turns: maxTurns,
|
|
365
504
|
phase,
|
|
366
|
-
|
|
505
|
+
is_stateless: true,
|
|
506
|
+
allowed_tools: allowedTools,
|
|
367
507
|
timeout_ms: timeoutMs
|
|
368
508
|
}
|
|
369
509
|
});
|
|
370
510
|
|
|
371
|
-
const { stdout } = await
|
|
372
|
-
const { result
|
|
511
|
+
const { stdout } = await spawnFn(args, timeoutMs);
|
|
512
|
+
const { result } = extractResultFromJson(stdout);
|
|
373
513
|
const parsed = parseSubagentResponse(result);
|
|
374
514
|
|
|
375
515
|
logger.debug('Claude subagent turn completed', {
|
|
@@ -379,91 +519,87 @@ async function runClaudeTurn(options) {
|
|
|
379
519
|
duration_ms: Date.now() - startAt,
|
|
380
520
|
message_length: parsed.message.length,
|
|
381
521
|
has_state_patch: Boolean(parsed.statePatch),
|
|
382
|
-
flag_count: parsed.flags.length
|
|
383
|
-
session_id: newSessionId || sessionId
|
|
522
|
+
flag_count: parsed.flags.length
|
|
384
523
|
}
|
|
385
524
|
});
|
|
386
525
|
|
|
387
526
|
return {
|
|
388
527
|
message: parsed.message,
|
|
389
528
|
statePatch: parsed.statePatch,
|
|
390
|
-
flags: parsed.flags
|
|
391
|
-
sessionId: newSessionId || sessionId
|
|
529
|
+
flags: parsed.flags
|
|
392
530
|
};
|
|
393
531
|
}
|
|
394
532
|
|
|
395
533
|
/**
|
|
396
|
-
* Run a summary turn
|
|
534
|
+
* Run a summary turn in stateless Claude mode.
|
|
397
535
|
*
|
|
398
|
-
* @param {
|
|
399
|
-
* @param {string}
|
|
536
|
+
* @param {Object} options
|
|
537
|
+
* @param {string} options.prompt - Unified summary prompt
|
|
538
|
+
* @param {string} [options.reason] - Why the conversation is ending
|
|
539
|
+
* @param {Array<string>} [options.capabilities] - Token capabilities for summary turn tooling
|
|
540
|
+
* @param {Array<string>} [options.allowedTopics] - Token allowed topics for summary turn tooling
|
|
541
|
+
* @param {function} [options.spawnFn] - Injectable process runner for tests
|
|
400
542
|
* @param {number} [timeoutMs=300000] - Timeout in milliseconds
|
|
401
543
|
* @returns {Promise<{ summary: string, ownerSummary: string, actionItems: array, flags: array }>}
|
|
402
544
|
*/
|
|
403
|
-
async function runClaudeSummary(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
545
|
+
async function runClaudeSummary(options = {}) {
|
|
546
|
+
const {
|
|
547
|
+
prompt,
|
|
548
|
+
reason,
|
|
549
|
+
capabilities = [],
|
|
550
|
+
allowedTopics = [],
|
|
551
|
+
spawnFn = spawnClaude,
|
|
552
|
+
timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
553
|
+
} = options;
|
|
409
554
|
|
|
410
|
-
|
|
555
|
+
const summaryPrompt = String(prompt || '').trim();
|
|
556
|
+
if (!summaryPrompt) {
|
|
557
|
+
throw new Error('Cannot summarize without a prompt');
|
|
558
|
+
}
|
|
411
559
|
|
|
412
|
-
|
|
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>`;
|
|
560
|
+
const allowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics });
|
|
561
|
+
const allowedToolsArg = buildClaudeToolArg(allowedTools);
|
|
422
562
|
|
|
423
563
|
const args = [
|
|
424
564
|
'-p',
|
|
425
565
|
'--output-format', 'json',
|
|
426
|
-
'--
|
|
427
|
-
summaryPrompt
|
|
566
|
+
'--model', DEFAULT_CLAUDE_MODEL,
|
|
428
567
|
];
|
|
429
568
|
|
|
569
|
+
if (allowedToolsArg) {
|
|
570
|
+
args.push('--allowedTools', allowedToolsArg);
|
|
571
|
+
}
|
|
572
|
+
args.push(
|
|
573
|
+
'--append-system-prompt',
|
|
574
|
+
`Conversation summary mode. Reason: ${reason || 'conversation ended'}. Return only structured summary JSON.`,
|
|
575
|
+
summaryPrompt
|
|
576
|
+
);
|
|
577
|
+
|
|
430
578
|
const startAt = Date.now();
|
|
431
579
|
|
|
432
580
|
logger.debug('Spawning Claude summary', {
|
|
433
581
|
event: 'subagent_summary_start',
|
|
434
|
-
data: {
|
|
582
|
+
data: {
|
|
583
|
+
reason: reason || 'conversation ended',
|
|
584
|
+
allowed_tools: allowedTools
|
|
585
|
+
}
|
|
435
586
|
});
|
|
436
587
|
|
|
437
|
-
const { stdout } = await
|
|
588
|
+
const { stdout } = await spawnFn(args, timeoutMs);
|
|
438
589
|
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
|
-
}
|
|
590
|
+
const summaryPayload = parseSummaryPayload(result);
|
|
591
|
+
const parsedSummary = summarizeFromPayload(summaryPayload, result.trim());
|
|
592
|
+
|
|
593
|
+
if (parsedSummary) {
|
|
594
|
+
logger.debug('Claude summary completed', {
|
|
595
|
+
event: 'subagent_summary_complete',
|
|
596
|
+
data: {
|
|
597
|
+
duration_ms: Date.now() - startAt,
|
|
598
|
+
has_summary: Boolean(parsedSummary.summary),
|
|
599
|
+
action_item_count: parsedSummary.actionItems.length
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
return parsedSummary;
|
|
467
603
|
}
|
|
468
604
|
|
|
469
605
|
// Fallback: use raw text as summary
|
|
@@ -480,6 +616,7 @@ module.exports = {
|
|
|
480
616
|
isClaudeAvailable,
|
|
481
617
|
buildSubagentSystemPrompt,
|
|
482
618
|
buildTurnPrompt,
|
|
619
|
+
resolveClaudeAllowedTools,
|
|
483
620
|
runClaudeTurn,
|
|
484
621
|
runClaudeSummary,
|
|
485
622
|
parseSubagentResponse
|
|
@@ -128,6 +128,10 @@ class ConversationDriver {
|
|
|
128
128
|
this.maxTurns = options.maxTurns || 30;
|
|
129
129
|
this.onTurn = options.onTurn || null;
|
|
130
130
|
this.tier = options.tier || 'public';
|
|
131
|
+
// Optional permission envelope for runtimes (primarily Claude mode).
|
|
132
|
+
// If provided by caller, this keeps tool allowlists variable per token/profile.
|
|
133
|
+
this.capabilities = Array.isArray(options.capabilities) ? options.capabilities : [];
|
|
134
|
+
this.allowedTopics = Array.isArray(options.allowedTopics) ? options.allowedTopics : [];
|
|
131
135
|
this.summarizer = options.summarizer || null;
|
|
132
136
|
this.ownerContext = options.ownerContext || {};
|
|
133
137
|
this.claudeMode = options.runtime?.mode === 'claude';
|
|
@@ -407,7 +411,10 @@ class ConversationDriver {
|
|
|
407
411
|
tier: this.tier,
|
|
408
412
|
ownerName: this.agentContext.owner,
|
|
409
413
|
agentName: this.agentContext.name,
|
|
410
|
-
roleContext: 'You initiated this call.'
|
|
414
|
+
roleContext: 'You initiated this call.',
|
|
415
|
+
capabilities: this.capabilities,
|
|
416
|
+
allowedTopics: this.allowedTopics,
|
|
417
|
+
allowed_topics: this.allowedTopics
|
|
411
418
|
};
|
|
412
419
|
if (this.claudeMode) {
|
|
413
420
|
contextPayload.turnCount = turn + 1;
|
|
@@ -149,7 +149,9 @@ function createRuntimeAdapter(options = {}) {
|
|
|
149
149
|
}
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
-
// Claude
|
|
152
|
+
// Claude state tracking.
|
|
153
|
+
// Design decision (A2A-29): we keep per-conversation state for prompt/metadata
|
|
154
|
+
// continuity, but Claude execution itself is stateless (no `--resume`).
|
|
153
155
|
const claudeSessions = new Map();
|
|
154
156
|
|
|
155
157
|
async function runClaudeTurnAdapter({ sessionId, message, caller, context, timeoutMs }) {
|
|
@@ -180,7 +182,18 @@ function createRuntimeAdapter(options = {}) {
|
|
|
180
182
|
roleContext: context?.roleContext || ''
|
|
181
183
|
});
|
|
182
184
|
|
|
183
|
-
session = {
|
|
185
|
+
session = {
|
|
186
|
+
systemPrompt,
|
|
187
|
+
turnCount: 0,
|
|
188
|
+
lastMeta: null,
|
|
189
|
+
// Keep a permission snapshot so summary runs with the same policy envelope.
|
|
190
|
+
permissionSnapshot: {
|
|
191
|
+
capabilities: Array.isArray(context?.capabilities) ? context.capabilities : [],
|
|
192
|
+
allowedTopics: Array.isArray(context?.allowedTopics || context?.allowed_topics)
|
|
193
|
+
? (context?.allowedTopics || context?.allowed_topics)
|
|
194
|
+
: []
|
|
195
|
+
}
|
|
196
|
+
};
|
|
184
197
|
claudeSessions.set(sessionId, session);
|
|
185
198
|
}
|
|
186
199
|
|
|
@@ -199,7 +212,6 @@ function createRuntimeAdapter(options = {}) {
|
|
|
199
212
|
});
|
|
200
213
|
|
|
201
214
|
const result = await invokeClaudeTurn({
|
|
202
|
-
sessionId: session.claudeSessionId,
|
|
203
215
|
systemPrompt: session.systemPrompt,
|
|
204
216
|
turnMessage: message,
|
|
205
217
|
turn: session.turnCount,
|
|
@@ -209,12 +221,21 @@ function createRuntimeAdapter(options = {}) {
|
|
|
209
221
|
activeThreads: context?.activeThreads || [],
|
|
210
222
|
candidateCollaborations: context?.candidateCollaborations || [],
|
|
211
223
|
closeSignal: context?.closeSignal || false,
|
|
224
|
+
capabilities: Array.isArray(context?.capabilities)
|
|
225
|
+
? context.capabilities
|
|
226
|
+
: (session.permissionSnapshot?.capabilities || []),
|
|
227
|
+
allowedTopics: Array.isArray(context?.allowedTopics || context?.allowed_topics)
|
|
228
|
+
? (context?.allowedTopics || context?.allowed_topics)
|
|
229
|
+
: (session.permissionSnapshot?.allowedTopics || []),
|
|
212
230
|
timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
213
231
|
});
|
|
214
232
|
|
|
215
|
-
//
|
|
216
|
-
if (
|
|
217
|
-
session.
|
|
233
|
+
// Update permission snapshot if the caller supplied explicit context this turn.
|
|
234
|
+
if (Array.isArray(context?.capabilities)) {
|
|
235
|
+
session.permissionSnapshot.capabilities = context.capabilities;
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(context?.allowedTopics || context?.allowed_topics)) {
|
|
238
|
+
session.permissionSnapshot.allowedTopics = context?.allowedTopics || context?.allowed_topics;
|
|
218
239
|
}
|
|
219
240
|
|
|
220
241
|
// Store flags/state for retrieval via getLastTurnMeta
|
|
@@ -385,20 +406,28 @@ function createRuntimeAdapter(options = {}) {
|
|
|
385
406
|
const requestId = callerInfo?.request_id || callerInfo?.requestId;
|
|
386
407
|
const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
|
|
387
408
|
|
|
388
|
-
// Claude mode:
|
|
409
|
+
// Claude mode: stateless summary invocation (no session restore dependency).
|
|
389
410
|
if (modeInfo.mode === 'claude') {
|
|
390
411
|
const session = claudeSessions.get(sessionId);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
412
|
+
const capabilities = session?.permissionSnapshot?.capabilities
|
|
413
|
+
|| callerInfo?.capabilities
|
|
414
|
+
|| [];
|
|
415
|
+
const allowedTopics = session?.permissionSnapshot?.allowedTopics
|
|
416
|
+
|| callerInfo?.allowedTopics
|
|
417
|
+
|| callerInfo?.allowed_topics
|
|
418
|
+
|| [];
|
|
419
|
+
|
|
420
|
+
const result = await runClaudeSummary({
|
|
421
|
+
prompt,
|
|
422
|
+
reason: 'conversation ended',
|
|
423
|
+
capabilities,
|
|
424
|
+
allowedTopics,
|
|
425
|
+
timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
426
|
+
});
|
|
427
|
+
if (result && result.summary) {
|
|
428
|
+
return result;
|
|
400
429
|
}
|
|
401
|
-
throw new Error('Claude summary
|
|
430
|
+
throw new Error('Claude summary returned empty result');
|
|
402
431
|
}
|
|
403
432
|
|
|
404
433
|
if (modeInfo.mode !== 'openclaw') {
|
package/src/routes/a2a.js
CHANGED
|
@@ -390,6 +390,9 @@ function createRoutes(options = {}) {
|
|
|
390
390
|
if (monitor) {
|
|
391
391
|
monitor.trackActivity(a2aContext.conversation_id, {
|
|
392
392
|
...sanitizedCaller,
|
|
393
|
+
tier: validation.tier,
|
|
394
|
+
capabilities: validation.capabilities,
|
|
395
|
+
allowed_topics: validation.allowed_topics,
|
|
393
396
|
trace_id: traceId,
|
|
394
397
|
request_id: requestId
|
|
395
398
|
});
|
package/src/server.js
CHANGED
|
@@ -621,7 +621,9 @@ async function callAgent(message, a2aContext) {
|
|
|
621
621
|
conversationId,
|
|
622
622
|
tier: tierInfo,
|
|
623
623
|
ownerName: agentContext.owner,
|
|
624
|
+
capabilities: Array.isArray(a2aContext.capabilities) ? a2aContext.capabilities : [],
|
|
624
625
|
allowedTopics: a2aContext.allowed_topics || [],
|
|
626
|
+
allowed_topics: a2aContext.allowed_topics || [],
|
|
625
627
|
timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
|
|
626
628
|
traceId,
|
|
627
629
|
requestId
|