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:
@ -337,6 +337,47 @@ impl From<PluginWorkspaceWriteBinaryDto> for application::PluginWorkspaceWriteBi
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin-owned storage read/delete request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginStorageGetDto {
|
||||
/// Plugin id owning the value.
|
||||
pub plugin_id: String,
|
||||
/// Plugin-owned key.
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
impl From<PluginStorageGetDto> for application::PluginStorageGetInput {
|
||||
fn from(value: PluginStorageGetDto) -> Self {
|
||||
Self {
|
||||
plugin_id: value.plugin_id,
|
||||
key: value.key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin-owned storage write request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginStorageSetDto {
|
||||
/// Plugin id owning the value.
|
||||
pub plugin_id: String,
|
||||
/// Plugin-owned key.
|
||||
pub key: String,
|
||||
/// JSON value to persist.
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
impl From<PluginStorageSetDto> for application::PluginStorageSetInput {
|
||||
fn from(value: PluginStorageSetDto) -> Self {
|
||||
Self {
|
||||
plugin_id: value.plugin_id,
|
||||
key: value.key,
|
||||
value: value.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin structured config document read request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -2377,8 +2418,8 @@ use application::{
|
||||
LaunchAgentOutput, ListAgentsOutput, ReadAgentContextOutput, ReadMcpToolPermissionsOutput,
|
||||
};
|
||||
use domain::{
|
||||
Agent, AgentMcpToolPolicyOverride, EffectivePermissions, McpToolPolicy, PermissionSet,
|
||||
ProjectPermissions, SkillKind, TerminalSession,
|
||||
Agent, AgentMcpToolPolicyOverride, EffectivePermissions, EffortSelection, McpToolPolicy,
|
||||
PermissionSet, PermissionShadowReport, ProjectPermissions, SkillKind, TerminalSession,
|
||||
};
|
||||
|
||||
/// One discoverable capability carried by an agent.
|
||||
@ -2413,6 +2454,8 @@ pub struct AgentDto {
|
||||
pub agent: Agent,
|
||||
/// Resolved discoverable capabilities.
|
||||
pub capabilities: Vec<AgentCapabilityDto>,
|
||||
/// Whether this agent is the effective project orchestrator.
|
||||
pub is_orchestrator: bool,
|
||||
}
|
||||
|
||||
impl AgentDto {
|
||||
@ -2422,6 +2465,7 @@ impl AgentDto {
|
||||
Self {
|
||||
agent,
|
||||
capabilities: Vec::new(),
|
||||
is_orchestrator: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2443,6 +2487,7 @@ impl From<ListAgentsOutput> for AgentListDto {
|
||||
.into_iter()
|
||||
.map(AgentCapabilityDto::from)
|
||||
.collect(),
|
||||
is_orchestrator: entry.is_orchestrator,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
@ -2512,6 +2557,25 @@ pub struct ProjectPermissionsDto(pub ProjectPermissions);
|
||||
#[serde(transparent)]
|
||||
pub struct EffectivePermissionsDto(pub EffectivePermissions);
|
||||
|
||||
/// Response for resolving one agent's file/bash permissions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResolveAgentPermissionsResponseDto {
|
||||
/// Resolved policy, or `null` when neither project nor agent policy exists.
|
||||
pub effective: Option<EffectivePermissions>,
|
||||
/// Diagnostic report for agent-level allows shadowed by project defaults.
|
||||
pub shadowed: PermissionShadowReport,
|
||||
}
|
||||
|
||||
impl From<application::ResolveAgentPermissionsOutput> for ResolveAgentPermissionsResponseDto {
|
||||
fn from(out: application::ResolveAgentPermissionsOutput) -> Self {
|
||||
Self {
|
||||
effective: out.effective,
|
||||
shadowed: out.shadowed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Full project system permission document crossing the wire.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
@ -2767,6 +2831,18 @@ pub struct ChangeAgentProfileRequestDto {
|
||||
pub cols: u16,
|
||||
}
|
||||
|
||||
/// Request DTO for `update_agent_effort`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateAgentEffortRequestDto {
|
||||
/// Id of the owning project.
|
||||
pub project_id: String,
|
||||
/// Id of the agent whose effort override changes.
|
||||
pub agent_id: String,
|
||||
/// `null` clears the override.
|
||||
pub effort: Option<EffortSelection>,
|
||||
}
|
||||
|
||||
/// Response DTO for `change_agent_profile`: the mutated agent plus the freshly
|
||||
/// relaunched session when a live session was hot-swapped (absent otherwise).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@ -3997,10 +4073,16 @@ pub struct CreateSkillRequestDto {
|
||||
pub project_id: String,
|
||||
/// Display name.
|
||||
pub name: String,
|
||||
/// Optional one-line affordance description.
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
/// Initial Markdown content.
|
||||
pub content: String,
|
||||
/// Scope the skill is created in.
|
||||
pub scope: SkillScope,
|
||||
/// Capability nature. Missing legacy clients create workflow skills.
|
||||
#[serde(default)]
|
||||
pub kind: SkillKind,
|
||||
}
|
||||
|
||||
/// Request DTO for `update_skill`.
|
||||
@ -4703,7 +4785,7 @@ pub struct SpawnBackgroundCommandRequestDto {
|
||||
mod tests {
|
||||
use application::McpToolPermissionCatalogue;
|
||||
use domain::mailbox::TicketId;
|
||||
use domain::{AgentId, ConversationId, ProjectMcpToolPermissions};
|
||||
use domain::{AgentId, ConversationId, PermissionShadowReport, ProjectMcpToolPermissions};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -4755,6 +4837,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_agent_permissions_response_dto_uses_effective_plus_shadowed_shape() {
|
||||
let dto = ResolveAgentPermissionsResponseDto {
|
||||
effective: None,
|
||||
shadowed: PermissionShadowReport {
|
||||
read: false,
|
||||
write: false,
|
||||
delete: false,
|
||||
execute_bash: true,
|
||||
fallback: true,
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(dto).unwrap(),
|
||||
json!({
|
||||
"effective": null,
|
||||
"shadowed": {
|
||||
"read": false,
|
||||
"write": false,
|
||||
"delete": false,
|
||||
"executeBash": true,
|
||||
"fallback": true
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_workspace_requests_use_stable_camel_case_contract() {
|
||||
let path = PluginWorkspacePathDto {
|
||||
@ -4868,6 +4978,42 @@ mod tests {
|
||||
assert_eq!(input.value["enabled"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_storage_requests_use_stable_camel_case_contract() {
|
||||
let get = PluginStorageGetDto {
|
||||
plugin_id: "dev.acme.gitgraph".to_owned(),
|
||||
key: "helloPlugin.launches".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&get).unwrap(),
|
||||
json!({
|
||||
"pluginId": "dev.acme.gitgraph",
|
||||
"key": "helloPlugin.launches"
|
||||
})
|
||||
);
|
||||
let input: application::PluginStorageGetInput = get.into();
|
||||
assert_eq!(input.plugin_id, "dev.acme.gitgraph");
|
||||
assert_eq!(input.key, "helloPlugin.launches");
|
||||
|
||||
let set = PluginStorageSetDto {
|
||||
plugin_id: "dev.acme.gitgraph".to_owned(),
|
||||
key: "helloPlugin.enabled".to_owned(),
|
||||
value: json!({"enabled": true}),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&set).unwrap(),
|
||||
json!({
|
||||
"pluginId": "dev.acme.gitgraph",
|
||||
"key": "helloPlugin.enabled",
|
||||
"value": {"enabled": true}
|
||||
})
|
||||
);
|
||||
let input: application::PluginStorageSetInput = set.into();
|
||||
assert_eq!(input.plugin_id, "dev.acme.gitgraph");
|
||||
assert_eq!(input.key, "helloPlugin.enabled");
|
||||
assert_eq!(input.value, json!({"enabled": true}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_workspace_outputs_use_stable_camel_case_contract() {
|
||||
let listing = PluginWorkspaceDirectoryListingDto {
|
||||
|
||||
Reference in New Issue
Block a user