feat(orchestrator): modèle de désignation d'orchestrateur + sink de diagnostic
Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).
Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -26,7 +26,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::conversation::ConversationParty;
|
||||
use domain::fileguard::{FileGuard, GuardError, GuardedResource};
|
||||
use domain::fileguard::{may_write_directly, FileGuard, GuardError, GuardedResource};
|
||||
use domain::markdown::MarkdownDoc;
|
||||
use domain::memory::{Memory, MemoryFrontmatter, MemorySlug, MemoryType};
|
||||
use domain::ports::{AgentContextStore, Clock, FileSystem, MemoryStore, RemotePath};
|
||||
@ -135,9 +135,10 @@ impl ReadContext {
|
||||
/// Proposes new content for an IdeA-owned context under the [`FileGuard`].
|
||||
///
|
||||
/// For an **agent** context: a direct write under an exclusive write-lease. For the
|
||||
/// **global** project context by a non-orchestrator: the guard returns
|
||||
/// [`GuardError::Forbidden`], which this use case turns into a *materialised proposal*
|
||||
/// (a file under `.ideai/proposals/`) — never an overwrite of the live context.
|
||||
/// **global** project context by a non-orchestrator: the use case asks the domain
|
||||
/// policy whether the requester may write directly; otherwise it materialises a
|
||||
/// proposal (a file under `.ideai/proposals/`) — never an overwrite of the live
|
||||
/// context.
|
||||
pub struct ProposeContext {
|
||||
guard: Arc<dyn FileGuard>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
@ -214,24 +215,23 @@ impl ProposeContext {
|
||||
Ok(ProposeOutcome::Written)
|
||||
}
|
||||
None => {
|
||||
// Global project context: single-writer. Try to acquire the write
|
||||
// lease; Forbidden ⇒ materialise a proposal instead of overwriting.
|
||||
match self
|
||||
.guard
|
||||
.acquire_write(requester, GuardedResource::ProjectContext)
|
||||
.await
|
||||
{
|
||||
Ok(_lease) => {
|
||||
let path = join_root(&project, PROJECT_CONTEXT_FILE);
|
||||
self.fs.write(&path, content.as_bytes()).await?;
|
||||
Ok(ProposeOutcome::Written)
|
||||
}
|
||||
Err(GuardError::Forbidden) => {
|
||||
let path = self.file_proposal(&project, requester, &content).await?;
|
||||
Ok(ProposeOutcome::Proposed { path })
|
||||
}
|
||||
Err(other) => Err(map_guard_err(other)),
|
||||
// Global project context: single-writer. Authorization stays in the
|
||||
// domain policy; the guard only serialises the eventual direct write.
|
||||
let manifest = self.contexts.load_manifest(&project).await?;
|
||||
let designation = manifest.orchestrator_designation();
|
||||
let resource = GuardedResource::ProjectContext;
|
||||
if !may_write_directly(requester, &resource, &designation) {
|
||||
let path = self.file_proposal(&project, requester, &content).await?;
|
||||
return Ok(ProposeOutcome::Proposed { path });
|
||||
}
|
||||
let _lease = self
|
||||
.guard
|
||||
.acquire_write(requester, resource)
|
||||
.await
|
||||
.map_err(map_guard_err)?;
|
||||
let path = join_root(&project, PROJECT_CONTEXT_FILE);
|
||||
self.fs.write(&path, content.as_bytes()).await?;
|
||||
Ok(ProposeOutcome::Written)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -392,7 +392,7 @@ mod tests {
|
||||
use async_trait::async_trait;
|
||||
use domain::agent::{AgentManifest, ManifestEntry};
|
||||
use domain::conversation::ConversationParty;
|
||||
use domain::fileguard::{may_write_directly, ReadLease, WriteLease};
|
||||
use domain::fileguard::{ReadLease, WriteLease};
|
||||
use domain::ports::{FsError, MemoryError, StoreError};
|
||||
use domain::project::ProjectPath;
|
||||
use domain::{ProfileId, ProjectId, RemoteRef};
|
||||
@ -443,12 +443,9 @@ mod tests {
|
||||
}
|
||||
async fn acquire_write(
|
||||
&self,
|
||||
who: ConversationParty,
|
||||
_who: ConversationParty,
|
||||
res: GuardedResource,
|
||||
) -> Result<WriteLease, GuardError> {
|
||||
if !may_write_directly(who, &res) {
|
||||
return Err(GuardError::Forbidden);
|
||||
}
|
||||
let lock = self.lock_for(&res);
|
||||
Ok(WriteLease::new(Box::new(lock.write_owned().await)))
|
||||
}
|
||||
@ -612,6 +609,7 @@ mod tests {
|
||||
Arc::new(FakeContexts {
|
||||
manifest: AgentManifest {
|
||||
version: 1,
|
||||
orchestrator: None,
|
||||
entries: vec![ManifestEntry {
|
||||
agent_id: agent,
|
||||
name: name.to_owned(),
|
||||
|
||||
Reference in New Issue
Block a user