fix(permissions): tools MCP inter-projets exposés (idea_project_*) + contactable agent/template persisté — update_agent_contactable (application/web-server/app-tauri), defaultContactable TemplateEditor save/reopen, groupes MCP 'IdeA inter-projets' avec fallback, permissions agent Contactable — #267 (QA verte)
This commit is contained in:
@ -522,6 +522,72 @@ pub struct UpdateAgentEffortOutput {
|
||||
pub agent: Agent,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UpdateAgentContactable
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`UpdateAgentContactable::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdateAgentContactableInput {
|
||||
/// The owning project.
|
||||
pub project: Project,
|
||||
/// The agent whose cross-project contactability changes.
|
||||
pub agent_id: AgentId,
|
||||
/// Whether agents from other IdeA projects may contact this agent.
|
||||
pub contactable: bool,
|
||||
}
|
||||
|
||||
/// Output of [`UpdateAgentContactable::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdateAgentContactableOutput {
|
||||
/// The updated agent.
|
||||
pub agent: Agent,
|
||||
}
|
||||
|
||||
/// Updates an agent's cross-project contactability in the project manifest.
|
||||
pub struct UpdateAgentContactable {
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
}
|
||||
|
||||
impl UpdateAgentContactable {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(contexts: Arc<dyn AgentContextStore>) -> Self {
|
||||
Self { contexts }
|
||||
}
|
||||
|
||||
/// Executes the update.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::NotFound`] if the agent is unknown to the project,
|
||||
/// - [`AppError::Invalid`] if the resulting manifest is invalid,
|
||||
/// - [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: UpdateAgentContactableInput,
|
||||
) -> Result<UpdateAgentContactableOutput, AppError> {
|
||||
let mut manifest = self.contexts.load_manifest(&input.project).await?;
|
||||
let entry = manifest
|
||||
.entries
|
||||
.iter_mut()
|
||||
.find(|entry| entry.agent_id == input.agent_id)
|
||||
.ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
|
||||
|
||||
let agent = entry
|
||||
.to_agent()
|
||||
.map_err(|err| AppError::Invalid(err.to_string()))?
|
||||
.with_contactable(input.contactable);
|
||||
*entry = ManifestEntry::from_agent(&agent);
|
||||
|
||||
let manifest = AgentManifest::new(manifest.version, manifest.entries)
|
||||
.map_err(|err| AppError::Invalid(err.to_string()))?;
|
||||
self.contexts
|
||||
.save_manifest(&input.project, &manifest)
|
||||
.await?;
|
||||
Ok(UpdateAgentContactableOutput { agent })
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets or clears an agent's per-agent effort override in the manifest.
|
||||
///
|
||||
/// The change applies at the agent's next launch; live sessions are not mutated.
|
||||
|
||||
@ -37,7 +37,8 @@ pub use lifecycle::{
|
||||
LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, ListedAgentCapabilities,
|
||||
LiveStateLeanProvider, ManagedTempExposureRequest, McpRuntime, PermissionProjectorRegistry,
|
||||
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
||||
StructuredRoutingMode, StructuredSessionDescriptor, UpdateAgentContext,
|
||||
StructuredRoutingMode, StructuredSessionDescriptor, UpdateAgentContactable,
|
||||
UpdateAgentContactableInput, UpdateAgentContactableOutput, UpdateAgentContext,
|
||||
UpdateAgentContextInput, UpdateAgentEffort, UpdateAgentEffortInput, UpdateAgentEffortOutput,
|
||||
AGENT_MEMORY_RECALL_BUDGET, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
||||
};
|
||||
|
||||
@ -66,8 +66,9 @@ pub use agent::{
|
||||
ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent, SaveOpenCodeProviderProfile,
|
||||
SaveOpenCodeProviderProfileInput, SaveOpenCodeProviderProfileOutput, SaveProfile,
|
||||
SaveProfileInput, SaveProfileOutput, SessionLimitService, StructuredRoutingMode,
|
||||
StructuredSessionDescriptor, TurnOutcome, UpdateAgentContext, UpdateAgentContextInput,
|
||||
UpdateAgentEffort, UpdateAgentEffortInput, UpdateAgentEffortOutput, AGENT_MEMORY_RECALL_BUDGET,
|
||||
StructuredSessionDescriptor, TurnOutcome, UpdateAgentContactable, UpdateAgentContactableInput,
|
||||
UpdateAgentContactableOutput, UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort,
|
||||
UpdateAgentEffortInput, UpdateAgentEffortOutput, AGENT_MEMORY_RECALL_BUDGET,
|
||||
CODEX_SUBMIT_DELAY_MS, LIVE_STATE_INJECT_MAX, RESUME_PROMPT,
|
||||
};
|
||||
pub use background::{
|
||||
|
||||
@ -57,8 +57,8 @@ use application::{
|
||||
DeleteAgent, DeleteAgentInput, LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput,
|
||||
LiveSessions, ManagedTempExposureRequest, PermissionProjectorRegistry, ReadAgentContext,
|
||||
ReadAgentContextInput, ReleaseManagedTemp, ResolveManagedTemp, StructuredRoutingMode,
|
||||
StructuredSessions, TerminalSessions, UpdateAgentContext, UpdateAgentContextInput,
|
||||
UpdateAgentEffort, UpdateAgentEffortInput,
|
||||
StructuredSessions, TerminalSessions, UpdateAgentContactable, UpdateAgentContactableInput,
|
||||
UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort, UpdateAgentEffortInput,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -1163,6 +1163,53 @@ async fn update_agent_effort_not_found_for_unknown_agent_id() {
|
||||
assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_agent_contactable_persists_manifest_and_can_be_cleared() {
|
||||
let a = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
let contexts = FakeContexts::with_agent(&a, "ctx");
|
||||
let update = UpdateAgentContactable::new(Arc::new(contexts.clone()));
|
||||
|
||||
let enabled = update
|
||||
.execute(UpdateAgentContactableInput {
|
||||
project: project(),
|
||||
agent_id: a.id,
|
||||
contactable: true,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(enabled.agent.contactable);
|
||||
assert!(contexts.manifest().entries[0].contactable);
|
||||
|
||||
let disabled = update
|
||||
.execute(UpdateAgentContactableInput {
|
||||
project: project(),
|
||||
agent_id: a.id,
|
||||
contactable: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!disabled.agent.contactable);
|
||||
assert!(!contexts.manifest().entries[0].contactable);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_agent_contactable_not_found_for_unknown_agent_id() {
|
||||
let a = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
let contexts = FakeContexts::with_agent(&a, "ctx");
|
||||
let update = UpdateAgentContactable::new(Arc::new(contexts));
|
||||
|
||||
let err = update
|
||||
.execute(UpdateAgentContactableInput {
|
||||
project: project(),
|
||||
agent_id: aid(404),
|
||||
contactable: true,
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_entry_then_unknown_is_not_found() {
|
||||
let a = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
|
||||
Reference in New Issue
Block a user