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:
@ -5,10 +5,17 @@
|
||||
//! concurrent readers **or** one exclusive writer per resource; different resources
|
||||
//! are independent (their locks are distinct).
|
||||
//!
|
||||
//! The single-writer rule for [`GuardedResource::ProjectContext`] is enforced
|
||||
//! **before** taking any lock: a `who` that is not the orchestrator
|
||||
//! ([`domain::may_write_directly`] returning `false`) is rejected with
|
||||
//! [`GuardError::Forbidden`] — it must *propose* instead.
|
||||
//! ## Pure lock — no authorization here (cadrage « orchestrateur du projet », Alt. A)
|
||||
//!
|
||||
//! This adapter is a **pure serialisation primitive**: `acquire_write` never returns
|
||||
//! [`GuardError::Forbidden`]. The single-writer *authorization* of the global
|
||||
//! [`GuardedResource::ProjectContext`] (« only the orchestrator may write it directly,
|
||||
//! everyone else proposes ») is **not** a concurrency concern and no longer lives in
|
||||
//! the guard. It is decided one layer up, in the `ProposeContext` use case, which
|
||||
//! consults the project's `OrchestratorDesignation` (now that the orchestrator can be
|
||||
//! a *designated agent*, not just the human, the guard — which only sees a
|
||||
//! [`ConversationParty`] — could not make that call coherently anyway). The guard's
|
||||
//! sole job is to make readers/writers of the same resource take turns.
|
||||
//!
|
||||
//! ## Cooperative scope (cadrage §9.5)
|
||||
//!
|
||||
@ -32,9 +39,7 @@ use async_trait::async_trait;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use domain::conversation::ConversationParty;
|
||||
use domain::fileguard::{
|
||||
may_write_directly, FileGuard, GuardError, GuardedResource, ReadLease, WriteLease,
|
||||
};
|
||||
use domain::fileguard::{FileGuard, GuardError, GuardedResource, ReadLease, WriteLease};
|
||||
|
||||
/// The [`FileGuard`] adapter: one reader/writer lock per guarded resource.
|
||||
///
|
||||
@ -85,14 +90,12 @@ impl FileGuard for RwFileGuard {
|
||||
|
||||
async fn acquire_write(
|
||||
&self,
|
||||
who: ConversationParty,
|
||||
_who: ConversationParty,
|
||||
res: GuardedResource,
|
||||
) -> Result<WriteLease, GuardError> {
|
||||
// Single-writer rule for the global project context: refuse *before* taking
|
||||
// any lock so a forbidden writer never blocks readers.
|
||||
if !may_write_directly(who, &res) {
|
||||
return Err(GuardError::Forbidden);
|
||||
}
|
||||
// Pure lock (Alt. A): no authorization here — `ProposeContext` decides who may
|
||||
// write the global context. Serialise behind any in-flight readers/writer of
|
||||
// the same resource, then hand back the exclusive RAII lease.
|
||||
let lock = self.lock_for(&res);
|
||||
let guard = lock.write_owned().await;
|
||||
Ok(WriteLease::new(Box::new(guard)))
|
||||
@ -170,22 +173,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_writing_project_context_is_forbidden() {
|
||||
async fn pure_lock_grants_project_context_write_to_any_party() {
|
||||
// Alt. A: the guard is a pure lock — it no longer enforces the single-writer
|
||||
// *authorization* (that moved into `ProposeContext`). Both the human and an
|
||||
// arbitrary agent obtain a write lease on the project context; `Forbidden` is
|
||||
// never produced at this layer.
|
||||
let guard = RwFileGuard::new();
|
||||
let err = guard
|
||||
assert!(guard
|
||||
.acquire_write(ConversationParty::User, GuardedResource::ProjectContext)
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(guard
|
||||
.acquire_write(agent_party(1), GuardedResource::ProjectContext)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err, GuardError::Forbidden);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn orchestrator_may_write_project_context() {
|
||||
let guard = RwFileGuard::new();
|
||||
let lease = guard
|
||||
.acquire_write(ConversationParty::User, GuardedResource::ProjectContext)
|
||||
.await;
|
||||
assert!(lease.is_ok());
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user