feat(config): Permission-Modus pro Liste und pro Task ueberschreibbar

Bisher gab es nur AppSettings.DefaultPermissionMode global — ein Task, der plan
oder acceptEdits braucht, erzwang das Umstellen der globalen Einstellung. Neue
Spalten tasks.permission_mode und list_config.permission_mode, Auflösung
task -> list -> global im EffectiveRunConfigResolver (den TaskRunner und
get_effective_run_config gemeinsam nutzen), ComboBox mit Inherited-Badge im
geteilten Agent-Editor.

MigrationBaselineTests: das Fixture baute per EnsureCreated das heutige Schema
und stempelte Legacy-History darauf — jede Migration nach dem Squash lief damit
in 'duplicate column name'. Es migriert jetzt gezielt bis InitialCreate und
prueft 'nichts pending' statt 'InitialCreate ist die einzige Zeile'.
This commit is contained in:
mika kuns
2026-08-27 10:28:57 +02:00
parent 5cc99bfec6
commit 4464dc6ff1
18 changed files with 1168 additions and 48 deletions
+17 -3
View File
@@ -544,6 +544,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
var agentPath = dto.AgentPath.NullIfBlank();
var sessionSkills = SkillsToJson(dto.SessionSkills);
var verifyCommand = dto.VerifyCommand.NullIfBlank();
var permissionMode = NormalizePermissionMode(dto.PermissionMode);
// A null SerializeOnFileOverlap means "leave it as stored" — only the list-settings modal
// owns that field, so every other caller must not drop it (neither by deleting the row nor
@@ -551,7 +552,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
var existing = await repo.GetConfigAsync(dto.ListId);
var serializeOnFileOverlap = dto.SerializeOnFileOverlap ?? existing?.SerializeOnFileOverlap ?? false;
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && !serializeOnFileOverlap)
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && permissionMode is null && !serializeOnFileOverlap)
{
await repo.DeleteConfigAsync(dto.ListId);
}
@@ -567,19 +568,31 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
SessionSkills = sessionSkills,
VerifyCommand = verifyCommand,
SerializeOnFileOverlap = serializeOnFileOverlap,
PermissionMode = permissionMode,
});
}
await _broadcaster.ListUpdated(dto.ListId);
}
/// Blank → null (inherit). Anything else must be a known mode: it becomes a
/// <c>--permission-mode</c> argument, and an unknown value only fails once a run starts.
private static string? NormalizePermissionMode(string? mode)
{
var value = mode.NullIfBlank();
if (value is null) return null;
if (!PermissionModeRegistry.Modes.Contains(value, StringComparer.OrdinalIgnoreCase))
throw new HubException($"unknown permission mode: {value}");
return value;
}
public async Task<ListConfigDto?> GetListConfig(string listId)
{
using var ctx = _dbFactory.CreateDbContext();
var repo = new ListRepository(ctx);
var config = await repo.GetConfigAsync(listId);
if (config is null) return null;
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand, config.SerializeOnFileOverlap);
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand, config.SerializeOnFileOverlap, config.PermissionMode);
}
public async Task<SetTaskStatusResultDto> SetTaskStatus(string taskId, string status)
@@ -674,7 +687,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
dto.SystemPrompt.NullIfBlank(),
dto.AgentPath.NullIfBlank(),
dto.MaxTurns,
SkillsToJson(dto.SessionSkills));
SkillsToJson(dto.SessionSkills),
NormalizePermissionMode(dto.PermissionMode));
await _broadcaster.TaskUpdated(dto.TaskId);
}