fix(runtime): isolation d'usage multi-projet #107
Implémentation du garde ModelServerProjectUseGuard pour séparer les retours des agents entre projets concurrents. - Garde RAII par LocalModelServerId partagé entre projets - Refus inter-projets concurrent via model_server_in_use - Partage intra-projet conservé avec refcount - Libération automatique à la fermeture/erreur de session Tests de validation: rejet projet distinct, compteur refs, libération retrait
This commit is contained in:
@ -35,7 +35,9 @@ use domain::live_state::WorkStatus;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::layout::{persist_doc, resolve_doc};
|
||||
use crate::model_server::{EnsureLocalModelServer, EnsureLocalModelServerInput};
|
||||
use crate::model_server::{
|
||||
EnsureLocalModelServer, EnsureLocalModelServerInput, ModelServerProjectUseGuard,
|
||||
};
|
||||
use crate::project::project_context_path;
|
||||
use crate::terminal::{StructuredSessions, TerminalSessions};
|
||||
use crate::workstate::GetLiveStateLean;
|
||||
@ -1782,7 +1784,8 @@ impl LaunchAgent {
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.ensure_local_model_server_for_opencode(&agent, &mut profile)
|
||||
let local_model_server_guard = self
|
||||
.ensure_local_model_server_for_opencode(&input.project, &agent, &mut profile)
|
||||
.await?;
|
||||
|
||||
// 5a. ── INJECTION DE LA CONF MCP (cadrage v3, Décision 3) ──
|
||||
@ -1886,6 +1889,7 @@ impl LaunchAgent {
|
||||
&spec.env,
|
||||
spec.sandbox.as_ref(),
|
||||
structured_policy.as_ref(),
|
||||
local_model_server_guard,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@ -1905,7 +1909,10 @@ impl LaunchAgent {
|
||||
|
||||
// 7. For the Stdin strategy, pipe the context once the PTY is live.
|
||||
if matches!(spec.context_plan, Some(ContextInjectionPlan::Stdin)) {
|
||||
self.pty.write(&handle, content.as_str().as_bytes())?;
|
||||
if let Err(err) = self.pty.write(&handle, content.as_str().as_bytes()) {
|
||||
let _ = self.pty.kill(&handle).await;
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
|
||||
let node_id = input.node_id.unwrap_or_else(NodeId::new_random);
|
||||
@ -1917,8 +1924,12 @@ impl LaunchAgent {
|
||||
size,
|
||||
);
|
||||
session.status = SessionStatus::Running;
|
||||
self.sessions
|
||||
.insert_in_project(input.project.id, handle, session.clone());
|
||||
self.sessions.insert_in_project_with_model_server_guard(
|
||||
input.project.id,
|
||||
handle,
|
||||
session.clone(),
|
||||
local_model_server_guard,
|
||||
);
|
||||
|
||||
self.events.publish(DomainEvent::AgentLaunched {
|
||||
agent_id: agent.id,
|
||||
@ -1964,6 +1975,7 @@ impl LaunchAgent {
|
||||
env: &[(String, String)],
|
||||
sandbox: Option<&SandboxPlan>,
|
||||
structured_policy: Option<&StructuredProviderLaunchPolicy>,
|
||||
model_server_guard: Option<ModelServerProjectUseGuard>,
|
||||
) -> Result<LaunchAgentOutput, AppError> {
|
||||
// Relaie le plan de sandbox OS (lot LP4-4) à la fabrique : `spec.sandbox`,
|
||||
// déjà compilé (pur, domaine) en step 5d. `None` ⇒ exécution native inchangée.
|
||||
@ -1986,7 +1998,13 @@ impl LaunchAgent {
|
||||
|
||||
// Enregistre la session vivante (invariant « 1 session/agent » : déjà gardé en
|
||||
// amont sur les deux registres).
|
||||
structured.insert_in_project(project_id, Arc::clone(&session), agent.id, node_id);
|
||||
structured.insert_in_project_with_model_server_guard(
|
||||
project_id,
|
||||
Arc::clone(&session),
|
||||
agent.id,
|
||||
node_id,
|
||||
model_server_guard,
|
||||
);
|
||||
|
||||
// ── SÉPARATION DES DEUX CLÉS (ARCHITECTURE §19.7, lot P8a) ──
|
||||
// - **id de paire** (`pair_conversation_id`) : clé **logique** persistée sur
|
||||
@ -2570,17 +2588,18 @@ impl LaunchAgent {
|
||||
|
||||
async fn ensure_local_model_server_for_opencode(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent: &Agent,
|
||||
profile: &mut AgentProfile,
|
||||
) -> Result<(), AppError> {
|
||||
) -> Result<Option<ModelServerProjectUseGuard>, AppError> {
|
||||
if profile.structured_adapter != Some(StructuredAdapter::OpenCode) {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(opencode) = profile.opencode.as_mut() else {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(server_id) = opencode.local_model_server_id else {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(ensure) = self.local_model_server.as_ref() else {
|
||||
let err = AppError::ModelServer {
|
||||
@ -2592,6 +2611,13 @@ impl LaunchAgent {
|
||||
self.publish_agent_launch_failed(agent.id, &err);
|
||||
return Err(err);
|
||||
};
|
||||
let guard = match ensure.acquire_project_use(server_id, project.id) {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
self.publish_agent_launch_failed(agent.id, &err);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
match ensure
|
||||
.execute(EnsureLocalModelServerInput { server_id })
|
||||
@ -2600,7 +2626,7 @@ impl LaunchAgent {
|
||||
Ok(output) => {
|
||||
opencode.base_url = output.ready.base_url;
|
||||
opencode.model = output.ready.model;
|
||||
Ok(())
|
||||
Ok(Some(guard))
|
||||
}
|
||||
Err(err) => {
|
||||
self.publish_agent_launch_failed(agent.id, &err);
|
||||
|
||||
Reference in New Issue
Block a user