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:
@ -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
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
use app_tauri_lib::dto::{
|
||||
parse_agent_id, AgentDto, AgentListDto, ConversationDetailsDto, CreateAgentRequestDto,
|
||||
InspectConversationRequestDto, LaunchAgentRequestDto, LiveAgentListDto, TerminalSessionDto,
|
||||
UpdateAgentContextRequestDto,
|
||||
UpdateAgentContextRequestDto, UpdateAgentEffortRequestDto,
|
||||
};
|
||||
use application::AppError;
|
||||
use application::{
|
||||
@ -17,7 +17,7 @@ use application::{
|
||||
use domain::ids::{AgentId, NodeId, ProfileId, SessionId};
|
||||
use domain::ports::ConversationDetails;
|
||||
use domain::terminal::{PtySize, SessionKind, SessionStatus, TerminalSession};
|
||||
use domain::{Agent, AgentOrigin, ProjectPath, SkillKind};
|
||||
use domain::{Agent, AgentOrigin, EffortSelection, ProjectPath, SkillKind};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -60,6 +60,15 @@ fn agent_dto_serialises_camelcase() {
|
||||
assert!(v.get("profile_id").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_dto_serialises_effort_tagged_shape_when_present() {
|
||||
let agent = make_agent(1, 2).with_effort(Some(EffortSelection::Preset("medium".to_owned())));
|
||||
let dto = AgentDto::from_agent(agent);
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
|
||||
assert_eq!(v["effort"], json!({"kind":"preset","value":"medium"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_list_dto_is_transparent_array() {
|
||||
let first = make_agent(1, 2);
|
||||
@ -73,6 +82,7 @@ fn agent_list_dto_is_transparent_array() {
|
||||
kind: SkillKind::Reference,
|
||||
}],
|
||||
}],
|
||||
effective_orchestrator: Some(first.id),
|
||||
};
|
||||
let dto = AgentListDto::from(out);
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
@ -82,7 +92,9 @@ fn agent_list_dto_is_transparent_array() {
|
||||
assert_eq!(arr[0]["capabilities"][0]["name"], "review");
|
||||
assert_eq!(arr[0]["capabilities"][0]["description"], "Reviews changes");
|
||||
assert_eq!(arr[0]["capabilities"][0]["kind"], "reference");
|
||||
assert_eq!(arr[0]["isOrchestrator"], true);
|
||||
assert_eq!(arr[1]["capabilities"], json!([]));
|
||||
assert_eq!(arr[1]["isOrchestrator"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -139,6 +151,28 @@ fn update_agent_context_request_deserialises_camelcase() {
|
||||
assert_eq!(dto.content, "# Updated");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_agent_effort_request_deserialises_and_null_clears() {
|
||||
let raw = json!({
|
||||
"projectId": Uuid::from_u128(1).to_string(),
|
||||
"agentId": Uuid::from_u128(2).to_string(),
|
||||
"effort": {"kind": "custom", "value": "x-deep"}
|
||||
});
|
||||
let dto: UpdateAgentEffortRequestDto = serde_json::from_value(raw).unwrap();
|
||||
assert_eq!(
|
||||
dto.effort,
|
||||
Some(EffortSelection::Custom("x-deep".to_owned()))
|
||||
);
|
||||
|
||||
let clear: UpdateAgentEffortRequestDto = serde_json::from_value(json!({
|
||||
"projectId": Uuid::from_u128(1).to_string(),
|
||||
"agentId": Uuid::from_u128(2).to_string(),
|
||||
"effort": null
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(clear.effort, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_agent_request_deserialises_camelcase() {
|
||||
let project_id = Uuid::from_u128(1).to_string();
|
||||
|
||||
Reference in New Issue
Block a user