feat(wave): #119/#122/#131/#132 verts + sprint plugins ESM/persistance #135/#136/#139

État d'intégration confiné à la branche batch. Les tickets #119 (skills →
capacités agent découvrables), #122 (override permissions par défaut), #131
(effort par agent/presets) et #132 (outil MCP d'édition du contexte projet)
sont verts en périmètre. Le sprint plugins multi-fichiers ESM / persistance
plugin-owned (#135/#136/#139) est co-implémenté dans les MÊMES fichiers de
câblage (frontend/src/ports/index.ts, backend/src/lib.rs, domain/ports.rs,
backend/dto.rs), inséparable sans staging interactif (indisponible ici).

Commit unique volontaire : préserve l'état vert QA sans découpe hunk risquée.
NON mergé vers develop tant que #137 (QA e2e plugins) n'est pas vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 11:06:23 +02:00
parent 22c6bd803d
commit 171c6c923c
59 changed files with 3654 additions and 291 deletions

View File

@ -25,9 +25,9 @@ use application::{
ResolveAgentSystemPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
RotateConversationLogInput, SetActiveLayoutInput, SnapshotRunningAgentsInput,
StopLiveAgentInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
UpdateAgentContextInput, UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
UpdateAgentSystemPermissionsInput, UpdateMemoryInput, UpdateProjectContextInput,
UpdateProjectMcpToolPermissionsInput, UpdateProjectPermissionsInput,
UpdateAgentContextInput, UpdateAgentEffortInput, UpdateAgentMcpToolPermissionsInput,
UpdateAgentPermissionsInput, UpdateAgentSystemPermissionsInput, UpdateMemoryInput,
UpdateProjectContextInput, UpdateProjectMcpToolPermissionsInput, UpdateProjectPermissionsInput,
UpdateProjectSystemPermissionsInput, UpdateSkillInput,
};
use backend::stream::OutputSink;
@ -47,28 +47,28 @@ use crate::dto::{
CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto,
CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto,
DeliveredDelegationRequestDto, DetectProfilesRequestDto, DetectProfilesResponseDto,
EffectivePermissionsDto, EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto,
ErrorDto, FirstRunStateDto, FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto,
GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto,
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto,
LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
ModelServerConfigDto, ModelServerConfigListDto, OpenCodeProviderListDto,
OpenTerminalRequestDto, PreviewModelServerCommandDto, ProfileDto, ProfileListDto,
ProfileModelCatalogDto, ProjectDto, ProjectListDto, ProjectMcpToolPermissionsDto,
ProjectPermissionsDto, ProjectSystemPermissionsDto, ProjectWorkStateDto,
ReadAgentContextResponseDto, ReadConversationPageRequestDto, ReattachChatDto,
ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto,
EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto, FirstRunStateDto,
FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto,
GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, GraphCommitListDto,
HealthRequestDto, HealthResponseDto, InspectConversationRequestDto, InterruptAgentRequestDto,
LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, LiveAgentListDto,
MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto,
ModelServerConfigListDto, OpenCodeProviderListDto, OpenTerminalRequestDto,
PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProfileModelCatalogDto, ProjectDto,
ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto,
ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto,
RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
ResolveAgentPermissionsRequestDto, ResolveAgentPermissionsResponseDto,
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveModelServerRequestDto,
SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
SetActiveLayoutResultDto, SkillDto, SkillListDto, StopLiveAgentRequestDto,
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
TemplateListDto, TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
UpdateAgentContextRequestDto, UpdateAgentMcpToolPermissionsRequestDto,
UpdateAgentPermissionsRequestDto, UpdateAgentSystemPermissionsRequestDto,
UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
UpdateAgentContextRequestDto, UpdateAgentEffortRequestDto,
UpdateAgentMcpToolPermissionsRequestDto, UpdateAgentPermissionsRequestDto,
UpdateAgentSystemPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
UpdateProjectMcpToolPermissionsRequestDto, UpdateProjectPermissionsRequestDto,
UpdateProjectSystemPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
WriteTerminalRequestDto,
@ -538,6 +538,29 @@ pub async fn update_agent_permissions(
.map_err(ErrorDto::from)
}
/// `update_agent_effort` — set or clear one agent's per-agent effort override.
///
/// # Errors
/// Returns an [`ErrorDto`] on invalid ids or store failure.
#[tauri::command]
pub async fn update_agent_effort(
request: UpdateAgentEffortRequestDto,
state: State<'_, AppState>,
) -> Result<AgentDto, ErrorDto> {
let project = resolve_project(&request.project_id, &state).await?;
let agent_id = parse_agent_id(&request.agent_id)?;
state
.update_agent_effort
.execute(UpdateAgentEffortInput {
project,
agent_id,
effort: request.effort,
})
.await
.map(|out| AgentDto::from_agent(out.agent))
.map_err(ErrorDto::from)
}
/// `resolve_agent_permissions` — resolve project defaults plus agent override.
///
/// # Errors
@ -546,14 +569,14 @@ pub async fn update_agent_permissions(
pub async fn resolve_agent_permissions(
request: ResolveAgentPermissionsRequestDto,
state: State<'_, AppState>,
) -> Result<Option<EffectivePermissionsDto>, ErrorDto> {
) -> Result<ResolveAgentPermissionsResponseDto, ErrorDto> {
let project = resolve_project(&request.project_id, &state).await?;
let agent_id = parse_agent_id(&request.agent_id)?;
state
.resolve_agent_permissions
.execute(ResolveAgentPermissionsInput { project, agent_id })
.await
.map(|out| out.effective.map(EffectivePermissionsDto))
.map(ResolveAgentPermissionsResponseDto::from)
.map_err(ErrorDto::from)
}
@ -3308,12 +3331,10 @@ pub async fn create_skill(
.create_skill
.execute(CreateSkillInput {
name: request.name,
// Description is set via the dedicated frontend field (T6); the create
// path stays None for now so the affordance falls back to the body's
// first line (see `Skill::effective_description`).
description: None,
description: request.description,
content: request.content,
scope: request.scope,
kind: request.kind,
project_root: project.root,
})
.await

View File

@ -257,6 +257,7 @@ pub fn run() {
commands::get_project_permissions,
commands::update_project_permissions,
commands::update_agent_permissions,
commands::update_agent_effort,
commands::resolve_agent_permissions,
commands::get_project_system_permissions,
commands::update_project_system_permissions,
@ -401,6 +402,9 @@ pub fn run() {
plugins::plugin_workspace_read_binary,
plugins::plugin_workspace_write_text,
plugins::plugin_workspace_write_binary,
plugins::plugin_storage_get,
plugins::plugin_storage_set,
plugins::plugin_storage_delete,
plugins::plugin_workspace_list_dir,
plugins::plugin_workspace_stat,
plugins::plugin_query_project_structure,
@ -438,6 +442,9 @@ fn plugin_workspace_invoke_handler<R: tauri::Runtime>(
plugins::plugin_workspace_read_binary,
plugins::plugin_workspace_write_text,
plugins::plugin_workspace_write_binary,
plugins::plugin_storage_get,
plugins::plugin_storage_set,
plugins::plugin_storage_delete,
plugins::plugin_workspace_list_dir,
plugins::plugin_workspace_stat,
plugins::plugin_query_project_structure,
@ -1030,6 +1037,61 @@ mod tests {
std::fs::remove_dir_all(app_data).ok();
}
#[test]
fn dto_plugins_storage_commands_are_registered_in_tauri_invoke_handler() {
let app_data = test_app_data_dir("plugin-storage-commands");
let app = mock_builder()
.manage(crate::state::AppState::build(app_data.clone()))
.invoke_handler(plugin_workspace_invoke_handler())
.build(mock_context(noop_assets()))
.expect("mock app builds");
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
.build()
.expect("mock webview builds");
let get_err = invoke_plugin_command(
&webview,
"plugin_storage_get",
json!({
"input": {
"pluginId": "dev.acme.missing",
"key": "helloPlugin.launches"
}
}),
)
.expect_err("missing plugin must surface through the registered command");
assert_eq!(get_err["code"], "NOT_FOUND");
let set_err = invoke_plugin_command(
&webview,
"plugin_storage_set",
json!({
"input": {
"pluginId": "dev.acme.missing",
"key": "helloPlugin.launches",
"value": 1
}
}),
)
.expect_err("missing plugin must surface through the registered command");
assert_eq!(set_err["code"], "NOT_FOUND");
let delete_err = invoke_plugin_command(
&webview,
"plugin_storage_delete",
json!({
"input": {
"pluginId": "dev.acme.missing",
"key": "helloPlugin.launches"
}
}),
)
.expect_err("missing plugin must surface through the registered command");
assert_eq!(delete_err["code"], "NOT_FOUND");
std::fs::remove_dir_all(app_data).ok();
}
#[test]
fn dto_plugins_command_task_commands_are_registered_in_tauri_invoke_handler() {
let app_data = test_app_data_dir("plugin-task-commands");

View File

@ -10,11 +10,11 @@ use backend::dto::{
PluginEventPollDto, PluginEventSubscribeDto, PluginEventSubscriptionDto,
PluginEventUnsubscribeDto, PluginInstallResultDto, PluginProjectStructureDto,
PluginProjectStructureQueryDto, PluginReviewDto, PluginRunCommandDto,
PluginRuntimeContributionCatalogDto, PluginTaskDto, PluginTaskStatusDto,
PluginToolchainDiagnosticDto, PluginToolchainDiagnosticRequestDto, PluginUninstallResultDto,
PluginWorkspaceBinaryFileDto, PluginWorkspaceDirectoryListingDto, PluginWorkspacePathDto,
PluginWorkspaceStatDto, PluginWorkspaceTextFileDto, PluginWorkspaceWriteBinaryDto,
PluginWorkspaceWriteTextDto, ReviewPluginPackageDto,
PluginRuntimeContributionCatalogDto, PluginStorageGetDto, PluginStorageSetDto, PluginTaskDto,
PluginTaskStatusDto, PluginToolchainDiagnosticDto, PluginToolchainDiagnosticRequestDto,
PluginUninstallResultDto, PluginWorkspaceBinaryFileDto, PluginWorkspaceDirectoryListingDto,
PluginWorkspacePathDto, PluginWorkspaceStatDto, PluginWorkspaceTextFileDto,
PluginWorkspaceWriteBinaryDto, PluginWorkspaceWriteTextDto, ReviewPluginPackageDto,
};
use domain::ports::{PluginManifestValidator, PluginPackageStore, PluginRegistryStore};
use domain::{PluginId, RelativePath};
@ -216,6 +216,45 @@ pub async fn plugin_workspace_write_binary(
.map_err(ErrorDto::from)
}
/// Reads a plugin-owned JSON storage value.
#[tauri::command]
pub async fn plugin_storage_get(
input: PluginStorageGetDto,
state: State<'_, AppState>,
) -> Result<Option<serde_json::Value>, ErrorDto> {
state
.plugin_storage_access
.get(input.into())
.await
.map_err(ErrorDto::from)
}
/// Writes a plugin-owned JSON storage value.
#[tauri::command]
pub async fn plugin_storage_set(
input: PluginStorageSetDto,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
state
.plugin_storage_access
.set(input.into())
.await
.map_err(ErrorDto::from)
}
/// Deletes a plugin-owned JSON storage value.
#[tauri::command]
pub async fn plugin_storage_delete(
input: PluginStorageGetDto,
state: State<'_, AppState>,
) -> Result<bool, ErrorDto> {
state
.plugin_storage_access
.delete(input.into())
.await
.map_err(ErrorDto::from)
}
/// Lists a workspace directory for the public plugin API.
#[tauri::command]
pub async fn plugin_workspace_list_dir(