fix(team): seed Write/Edit/NotebookEdit permissions before CLI launch

seedTeammateOperationalPermissionRules already pre-writes MCP tool
rules to settings.local.json before spawning the CLI. But standard
file tools (Write, Edit, NotebookEdit) were missing.

FACT: Teammates requesting Write get setMode: acceptEdits suggestion
but we can't change subprocess session mode. Pre-seeding these tools
as allow rules prevents the permission prompt entirely.
This commit is contained in:
iliya 2026-03-30 22:58:22 +03:00
parent bd242fac5a
commit f7876d89f7
2 changed files with 28 additions and 7 deletions

View file

@ -6541,11 +6541,16 @@ export class TeamProvisioningService {
): Promise<void> {
const settingsPath = path.join(projectCwd, '.claude', 'settings.local.json');
try {
const added = await this.addPermissionRulesToSettings(
settingsPath,
[...AGENT_TEAMS_NAMESPACED_TEAMMATE_OPERATIONAL_TOOL_NAMES],
'allow'
);
// FACT: Teammates need both MCP tools AND standard file tools (Write/Edit).
// FACT: Standard tools use "setMode: acceptEdits" permission_suggestions, but
// we can't change subprocess session mode — so we pre-add them as allow rules.
const allTools = [
...AGENT_TEAMS_NAMESPACED_TEAMMATE_OPERATIONAL_TOOL_NAMES,
'Edit',
'Write',
'NotebookEdit',
];
const added = await this.addPermissionRulesToSettings(settingsPath, allTools, 'allow');
logger.info(
`[${teamName}] Seeded teammate operational MCP rules in ${settingsPath} (${added} added)`
);

View file

@ -8,6 +8,7 @@
*/
import { agentAvatarUrl } from '@renderer/utils/memberHelpers';
import { getInboxJsonType, isInboxNoiseMessage } from '@shared/utils/inboxNoise';
import { isLeadMember } from '@shared/utils/leadDetection';
import type {
@ -524,6 +525,15 @@ export class TeamGraphAdapter {
// Skip comment notifications — #buildCommentParticles handles them with real text
if (msg.summary?.startsWith('Comment on ')) continue;
// Handle noise messages: idle shows as "idle", others (shutdown, terminated) skip entirely
const msgText = msg.text ?? '';
const noiseType = getInboxJsonType(msgText);
if (noiseType === 'idle_notification') {
// Show idle as a simple label, don't skip
} else if (isInboxNoiseMessage(msgText)) {
continue; // skip shutdown_approved, teammate_terminated, shutdown_request
}
const edgeId = TeamGraphAdapter.#resolveMessageEdge(msg, teamName, leadId, leadName, edges);
if (!edgeId) continue;
@ -537,13 +547,19 @@ export class TeamGraphAdapter {
);
const isFromTeammate = fromId !== leadId;
// For idle notifications, show a clean "idle" label instead of raw JSON
const particleLabel =
noiseType === 'idle_notification'
? 'idle'
: TeamGraphAdapter.#buildParticleLabel(msg.summary ?? msg.text, 'inbox');
particles.push({
id: `particle:msg:${teamName}:${msgKey}`,
edgeId,
progress: 0,
kind: 'inbox_message',
color: msg.color ?? '#66ccff',
label: TeamGraphAdapter.#buildParticleLabel(msg.summary ?? msg.text, 'inbox'),
label: particleLabel,
reverse: isFromTeammate,
});
}
@ -735,7 +751,7 @@ export class TeamGraphAdapter {
static #buildParticleLabel(
text: string | undefined,
kind: 'inbox' | 'comment',
max = 26
max = 52
): string | undefined {
const normalized = text?.replace(/\s+/g, ' ').trim();
const prefix = kind === 'comment' ? '\u{1F4AC}' : '\u{2709}';