Skip to content

Commit be55892

Browse files
committed
fix(team): complete teammate permission responses
1 parent 471fe0a commit be55892

4 files changed

Lines changed: 229 additions & 86 deletions

File tree

src/main/services/team/TeamProvisioningService.ts

Lines changed: 80 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -31999,103 +31999,100 @@ export class TeamProvisioningService {
3199931999
});
3200032000
};
3200132001

32002-
// Apply permission_suggestions: add tool rules to project settings file
32002+
// Apply permission_suggestions: add tool rules to project settings file.
3200332003
if (suggestions.length === 0) {
32004-
sendSuccessResponse();
3200532004
logger.info(
32006-
`[${run.teamName}] No permission_suggestions for ${requestId}; inbox response sent`
32005+
`[${run.teamName}] No permission_suggestions for ${requestId}; sending allow responses only`
3200732006
);
32008-
return;
32009-
}
32007+
} else {
32008+
// Resolve project cwd from team config
32009+
let projectCwd: string | undefined;
32010+
try {
32011+
const config = await this.readConfigForStrictDecision(run.teamName);
32012+
projectCwd = config?.projectPath ?? config?.members?.[0]?.cwd;
32013+
} catch {
32014+
// best-effort
32015+
}
3201032016

32011-
// Resolve project cwd from team config
32012-
let projectCwd: string | undefined;
32013-
try {
32014-
const config = await this.readConfigForStrictDecision(run.teamName);
32015-
projectCwd = config?.projectPath ?? config?.members?.[0]?.cwd;
32016-
} catch {
32017-
// best-effort
32018-
}
32019-
if (!projectCwd) {
32020-
logger.warn(
32021-
`[${run.teamName}] Cannot resolve project cwd for permission rule; sending inbox response only`
32022-
);
32023-
sendSuccessResponse();
32024-
return;
32025-
}
32017+
if (!projectCwd) {
32018+
logger.warn(
32019+
`[${run.teamName}] Cannot resolve project cwd for permission rule; sending allow responses only`
32020+
);
32021+
} else {
32022+
for (const suggestion of suggestions) {
32023+
// Handle "setMode" suggestions (e.g. Write/Edit tools suggest acceptEdits mode)
32024+
// FACT: Write/Edit permission_requests have permission_suggestions:
32025+
// { type: "setMode", mode: "acceptEdits", destination: "session" }
32026+
// Since we can't change session mode of a subprocess, we translate to addRules.
32027+
if (suggestion.type === 'setMode') {
32028+
const mode = typeof suggestion.mode === 'string' ? suggestion.mode : '';
32029+
let toolNames: string[] = [];
32030+
if (mode === 'acceptEdits') {
32031+
toolNames = ['Edit', 'Write', 'NotebookEdit'];
32032+
} else if (mode === 'bypassPermissions') {
32033+
// Broad approval - add common tools
32034+
toolNames = ['Edit', 'Write', 'NotebookEdit', 'Bash', 'Read', 'Grep', 'Glob'];
32035+
}
32036+
if (toolNames.length > 0) {
32037+
const settingsPath = path.join(projectCwd, '.claude', 'settings.local.json');
32038+
try {
32039+
await this.addPermissionRulesToSettings(settingsPath, toolNames, 'allow');
32040+
logger.info(
32041+
`[${run.teamName}] Applied setMode "${mode}" for ${agentId}: ${toolNames.join(', ')} in ${settingsPath}`
32042+
);
32043+
} catch (error) {
32044+
logger.error(
32045+
`[${run.teamName}] Failed to apply setMode: ${
32046+
error instanceof Error ? error.message : String(error)
32047+
}`
32048+
);
32049+
}
32050+
}
32051+
continue;
32052+
}
32053+
32054+
if (suggestion.type !== 'addRules' || !Array.isArray(suggestion.rules)) continue;
32055+
32056+
let toolNames = suggestion.rules
32057+
.map((r) => r.toolName)
32058+
.filter((name): name is string => typeof name === 'string' && name.length > 0);
32059+
if (toolNames.length === 0) continue;
32060+
32061+
// Expand teammate-safe operational tools only.
32062+
// This removes the bootstrap/task workflow race without accidentally granting
32063+
// admin/runtime tools like team_stop or kanban_clear.
32064+
if (
32065+
toolNames.some((name) =>
32066+
AGENT_TEAMS_NAMESPACED_TEAMMATE_OPERATIONAL_TOOL_NAMES.includes(name)
32067+
)
32068+
) {
32069+
const merged = new Set([
32070+
...toolNames,
32071+
...AGENT_TEAMS_NAMESPACED_TEAMMATE_OPERATIONAL_TOOL_NAMES,
32072+
]);
32073+
toolNames = Array.from(merged);
32074+
}
32075+
32076+
const behavior = suggestion.behavior ?? 'allow';
32077+
// FACT: observed destinations are "localSettings" (project-level .claude/settings.local.json)
32078+
const settingsPath =
32079+
suggestion.destination === 'localSettings'
32080+
? path.join(projectCwd, '.claude', 'settings.local.json')
32081+
: path.join(projectCwd, '.claude', 'settings.local.json'); // default to local
3202632082

32027-
for (const suggestion of suggestions) {
32028-
// Handle "setMode" suggestions (e.g. Write/Edit tools suggest acceptEdits mode)
32029-
// FACT: Write/Edit permission_requests have permission_suggestions:
32030-
// { type: "setMode", mode: "acceptEdits", destination: "session" }
32031-
// Since we can't change session mode of a subprocess, we translate to addRules.
32032-
if (suggestion.type === 'setMode') {
32033-
const mode = typeof suggestion.mode === 'string' ? suggestion.mode : '';
32034-
let toolNames: string[] = [];
32035-
if (mode === 'acceptEdits') {
32036-
toolNames = ['Edit', 'Write', 'NotebookEdit'];
32037-
} else if (mode === 'bypassPermissions') {
32038-
// Broad approval - add common tools
32039-
toolNames = ['Edit', 'Write', 'NotebookEdit', 'Bash', 'Read', 'Grep', 'Glob'];
32040-
}
32041-
if (toolNames.length > 0) {
32042-
const settingsPath = path.join(projectCwd, '.claude', 'settings.local.json');
3204332083
try {
32044-
await this.addPermissionRulesToSettings(settingsPath, toolNames, 'allow');
32084+
await this.addPermissionRulesToSettings(settingsPath, toolNames, behavior);
3204532085
logger.info(
32046-
`[${run.teamName}] Applied setMode "${mode}" for ${agentId}: ${toolNames.join(', ')} in ${settingsPath}`
32086+
`[${run.teamName}] Added permission rules for ${agentId}: ${toolNames.join(', ')} -> ${behavior} in ${settingsPath}`
3204732087
);
3204832088
} catch (error) {
3204932089
logger.error(
32050-
`[${run.teamName}] Failed to apply setMode: ${
32090+
`[${run.teamName}] Failed to add permission rules: ${
3205132091
error instanceof Error ? error.message : String(error)
3205232092
}`
3205332093
);
3205432094
}
3205532095
}
32056-
continue;
32057-
}
32058-
32059-
if (suggestion.type !== 'addRules' || !Array.isArray(suggestion.rules)) continue;
32060-
32061-
let toolNames = suggestion.rules
32062-
.map((r) => r.toolName)
32063-
.filter((name): name is string => typeof name === 'string' && name.length > 0);
32064-
if (toolNames.length === 0) continue;
32065-
32066-
// Expand teammate-safe operational tools only.
32067-
// This removes the bootstrap/task workflow race without accidentally granting
32068-
// admin/runtime tools like team_stop or kanban_clear.
32069-
if (
32070-
toolNames.some((name) =>
32071-
AGENT_TEAMS_NAMESPACED_TEAMMATE_OPERATIONAL_TOOL_NAMES.includes(name)
32072-
)
32073-
) {
32074-
const merged = new Set([
32075-
...toolNames,
32076-
...AGENT_TEAMS_NAMESPACED_TEAMMATE_OPERATIONAL_TOOL_NAMES,
32077-
]);
32078-
toolNames = Array.from(merged);
32079-
}
32080-
32081-
const behavior = suggestion.behavior ?? 'allow';
32082-
// FACT: observed destinations are "localSettings" (project-level .claude/settings.local.json)
32083-
const settingsPath =
32084-
suggestion.destination === 'localSettings'
32085-
? path.join(projectCwd, '.claude', 'settings.local.json')
32086-
: path.join(projectCwd, '.claude', 'settings.local.json'); // default to local
32087-
32088-
try {
32089-
await this.addPermissionRulesToSettings(settingsPath, toolNames, behavior);
32090-
logger.info(
32091-
`[${run.teamName}] Added permission rules for ${agentId}: ${toolNames.join(', ')} -> ${behavior} in ${settingsPath}`
32092-
);
32093-
} catch (error) {
32094-
logger.error(
32095-
`[${run.teamName}] Failed to add permission rules: ${
32096-
error instanceof Error ? error.message : String(error)
32097-
}`
32098-
);
3209932096
}
3210032097
}
3210132098

@@ -32186,7 +32183,7 @@ export class TeamProvisioningService {
3218632183
message: string | undefined
3218732184
): Record<string, unknown> | undefined {
3218832185
if (!toolInput) return undefined;
32189-
if (toolName !== 'AskUserQuestion' || !message) return toolInput;
32186+
if (toolName !== 'AskUserQuestion' || message === undefined) return toolInput;
3219032187

3219132188
const answers = this.parseAskUserQuestionAnswers(message, toolInput);
3219232189
return Object.keys(answers).length > 0 ? { ...toolInput, answers } : toolInput;

src/renderer/utils/__tests__/teamModelAvailability.codexCatalog.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,34 @@ describe('team model availability Codex catalog integration', () => {
147147
availabilityStatus: 'available',
148148
availabilityReason: null,
149149
},
150+
{
151+
value: 'gpt-5.3-codex-spark',
152+
label: '5.3 Codex Spark',
153+
badgeLabel: undefined,
154+
availabilityStatus: null,
155+
availabilityReason: null,
156+
},
157+
{
158+
value: 'gpt-5.2-codex',
159+
label: '5.2 Codex',
160+
badgeLabel: undefined,
161+
availabilityStatus: null,
162+
availabilityReason: null,
163+
},
164+
{
165+
value: 'gpt-5.1-codex-mini',
166+
label: '5.1 Codex Mini',
167+
badgeLabel: undefined,
168+
availabilityStatus: null,
169+
availabilityReason: null,
170+
},
171+
{
172+
value: 'gpt-5.1-codex-max',
173+
label: '5.1 Codex Max',
174+
badgeLabel: undefined,
175+
availabilityStatus: null,
176+
availabilityReason: null,
177+
},
150178
]);
151179
});
152180

@@ -224,7 +252,16 @@ describe('team model availability Codex catalog integration', () => {
224252

225253
expect(
226254
getAvailableTeamProviderModelOptions('codex', providerStatus).map((model) => model.value)
227-
).toEqual(['', 'gpt-5.5', 'gpt-5.4', 'gpt-5.2']);
255+
).toEqual([
256+
'',
257+
'gpt-5.5',
258+
'gpt-5.4',
259+
'gpt-5.3-codex-spark',
260+
'gpt-5.2',
261+
'gpt-5.2-codex',
262+
'gpt-5.1-codex-mini',
263+
'gpt-5.1-codex-max',
264+
]);
228265
});
229266

230267
it('keeps existing disabled model policy on top of the dynamic catalog', () => {

test/main/services/team/TeamProvisioningService.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14454,6 +14454,26 @@ describe('TeamProvisioningService', () => {
1445414454
});
1445514455
});
1445614456

14457+
it('preserves blank teammate AskUserQuestion answers', () => {
14458+
const svc = new TeamProvisioningService();
14459+
const toolInput = {
14460+
questions: [
14461+
{
14462+
question: 'Anything else?',
14463+
options: [{ label: 'Skip', description: 'No extra details' }],
14464+
},
14465+
],
14466+
};
14467+
14468+
expect((svc as any).buildTeammatePermissionUpdatedInput('AskUserQuestion', toolInput, ''))
14469+
.toEqual({
14470+
...toolInput,
14471+
answers: {
14472+
'Anything else?': '',
14473+
},
14474+
});
14475+
});
14476+
1445714477
it('sends teammate AskUserQuestion permission responses to the teammate inbox', async () => {
1445814478
const svc = new TeamProvisioningService();
1445914479
const persistInboxMessage = vi.fn();
@@ -14570,6 +14590,45 @@ describe('TeamProvisioningService', () => {
1457014590
});
1457114591
});
1457214592

14593+
it('sends teammate fallback control responses without permission suggestions', async () => {
14594+
const write = vi.fn((_line: string, cb?: (error?: Error | null) => void) => {
14595+
cb?.();
14596+
return true;
14597+
});
14598+
const svc = new TeamProvisioningService();
14599+
(svc as any).persistInboxMessage = vi.fn();
14600+
const toolInput = {
14601+
questions: [
14602+
{
14603+
question: 'Anything else?',
14604+
options: [{ label: 'Skip', description: 'No extra details' }],
14605+
},
14606+
],
14607+
};
14608+
14609+
await (svc as any).respondToTeammatePermission(
14610+
{
14611+
teamName: 'ops-team',
14612+
runId: 'run-1',
14613+
child: { stdin: { writable: true, write } },
14614+
},
14615+
'bob',
14616+
'perm-3',
14617+
true,
14618+
'',
14619+
[],
14620+
'AskUserQuestion',
14621+
toolInput
14622+
);
14623+
14624+
expect(write).toHaveBeenCalledTimes(1);
14625+
const payload = JSON.parse(write.mock.calls[0][0]);
14626+
expect(payload.response.response.updatedInput).toEqual({
14627+
...toolInput,
14628+
answers: { 'Anything else?': '' },
14629+
});
14630+
});
14631+
1457314632
it('uses a non-alarming model delay message before 2 minutes of silence', () => {
1457414633
const svc = new TeamProvisioningService();
1457514634

test/renderer/utils/teamModelAvailability.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,46 @@ describe('teamModelAvailability', () => {
111111
);
112112
});
113113

114-
it('builds Codex model options from the runtime list instead of the hardcoded fallback', () => {
114+
it('builds Codex model options from the runtime list plus disabled safety entries', () => {
115115
const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.3-codex']);
116116

117117
expect(getAvailableTeamProviderModelOptions('codex', providerStatus)).toEqual([
118118
{ value: '', label: 'Default', badgeLabel: 'Default' },
119-
{ value: 'gpt-5.4', label: '5.4', availabilityStatus: 'available', availabilityReason: null },
119+
{
120+
value: 'gpt-5.4',
121+
label: '5.4',
122+
badgeLabel: undefined,
123+
availabilityStatus: 'available',
124+
availabilityReason: null,
125+
},
120126
{
121127
value: 'gpt-5.3-codex',
122128
label: '5.3 Codex',
129+
badgeLabel: undefined,
123130
availabilityStatus: 'available',
124131
availabilityReason: null,
125132
},
133+
{
134+
value: 'gpt-5.3-codex-spark',
135+
label: '5.3 Codex Spark',
136+
badgeLabel: undefined,
137+
availabilityStatus: null,
138+
availabilityReason: null,
139+
},
140+
{
141+
value: 'gpt-5.2-codex',
142+
label: '5.2 Codex',
143+
badgeLabel: undefined,
144+
availabilityStatus: null,
145+
availabilityReason: null,
146+
},
147+
{
148+
value: 'gpt-5.1-codex-mini',
149+
label: '5.1 Codex Mini',
150+
badgeLabel: undefined,
151+
availabilityStatus: null,
152+
availabilityReason: null,
153+
},
126154
]);
127155
});
128156

@@ -148,9 +176,31 @@ describe('teamModelAvailability', () => {
148176
{
149177
value: 'gpt-5.4',
150178
label: '5.4',
179+
badgeLabel: undefined,
151180
availabilityStatus: 'unavailable',
152181
availabilityReason: 'No access for this account',
153182
},
183+
{
184+
value: 'gpt-5.3-codex-spark',
185+
label: '5.3 Codex Spark',
186+
badgeLabel: undefined,
187+
availabilityStatus: null,
188+
availabilityReason: null,
189+
},
190+
{
191+
value: 'gpt-5.2-codex',
192+
label: '5.2 Codex',
193+
badgeLabel: undefined,
194+
availabilityStatus: null,
195+
availabilityReason: null,
196+
},
197+
{
198+
value: 'gpt-5.1-codex-mini',
199+
label: '5.1 Codex Mini',
200+
badgeLabel: undefined,
201+
availabilityStatus: null,
202+
availabilityReason: null,
203+
},
154204
]);
155205
});
156206

0 commit comments

Comments
 (0)