feat(backend): appareils appairés persistants, révocables et code éphémère (#77 B1-B4)
L'appairage ne survivait pas au redémarrage et son code, permanent, était imprimé sur la sortie standard. Un appareil appairé devient une entité persistante, nommée et révocable, derrière un code désormais éphémère. - B1 : port DeviceSessionStore et adapter FsDeviceSessionStore, entités de domaine (PairedDevice, DeviceId, SessionTokenHash, DeviceName). Les tokens sont hachés en SHA-256 et comparés en temps constant (subtle) : le store ne peut pas rejouer une session qu'il a servie. Cookie Max-Age 400 j à renouvellement glissant, lastSeenAtMs throttlé. - B2 : code éphémère en mémoire, TTL 10 min et usage unique, toute génération invalidant la précédente. POST /api/pairing-code authentifiée, flag --new-code. Le code est retiré du boot et l'eprintln! qui l'imprimait est supprimé. - B3 : endpoints devices (list/rename/revoke/revoke-all/logout), event DeviceRevoked et ActiveConnectionRegistry par device_id, fermant sans délai les WebSockets d'un appareil révoqué. - B4 : port PairAttemptLimiter et adapter mémoire, rate-limit par origine et global sur horloge injectée, donc testable sans attente réelle. La normalisation du code passe côté serveur : elle absorbe la dette #76, que la seule normalisation frontend de #75 ne faisait que masquer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -302,6 +302,14 @@ pub enum DomainEventDto {
|
||||
/// Version synced to.
|
||||
to: u64,
|
||||
},
|
||||
/// A paired device was revoked.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
DeviceRevoked {
|
||||
/// Device id.
|
||||
device_id: String,
|
||||
},
|
||||
/// Every paired device was revoked.
|
||||
AllDevicesRevoked,
|
||||
/// A skill was assigned to (or unassigned from) an agent.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SkillAssigned {
|
||||
@ -884,6 +892,10 @@ impl From<&DomainEvent> for DomainEventDto {
|
||||
agent_id: agent_id.to_string(),
|
||||
to: to.get(),
|
||||
},
|
||||
DomainEvent::DeviceRevoked { device_id } => Self::DeviceRevoked {
|
||||
device_id: device_id.to_string(),
|
||||
},
|
||||
DomainEvent::AllDevicesRevoked => Self::AllDevicesRevoked,
|
||||
DomainEvent::SkillAssigned {
|
||||
agent_id,
|
||||
skill_id,
|
||||
|
||||
@ -13,43 +13,46 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use application::{
|
||||
AgentResumer, AgentWakeService, AppError, AssignIssueAgent, AssignSkillToAgent,
|
||||
AssignTicketToSprint, AttachLiveAgent, BackgroundCommandArchive, CancelBackgroundTask,
|
||||
ChangeAgentProfile, CheckEmbedderSuggestion, CloneOpenCodeProfileFromSeed, CloseProject,
|
||||
CloseTab, CloseTerminal, CloseTicketAssistant, ConfigureProfiles, ContextGuardUseCases,
|
||||
CreateAgentFromScratch, CreateAgentFromTemplate, CreateIssue, CreateLayout, CreateMemory,
|
||||
CreateProject, CreateSkill, CreateSprint, CreateTemplate, DeleteAgent, DeleteEmbedderProfile,
|
||||
DeleteIssue, DeleteLayout, DeleteMemory, DeleteModelServer, DeleteProfile, DeleteSkill,
|
||||
DeleteSprint, DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles,
|
||||
DismissEmbedderSuggestion, EnsureLocalModelServer, FirstRunState, GetLiveStateLean, GetMemory,
|
||||
GetProjectPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph,
|
||||
GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase,
|
||||
InspectConversation, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput,
|
||||
ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers, ListProfiles,
|
||||
ListProjects, ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry,
|
||||
LiveSessions, LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout,
|
||||
McpRuntime, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
|
||||
OpenTicketAssistant, OrchestratorService, PermissionProjectorRegistry, ProposeContext,
|
||||
ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMemory,
|
||||
ReadMemoryIndex, ReadProjectContext, ReadSkill, RecallMemory, ReconcileLayouts,
|
||||
ReconcileLiveState, ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles,
|
||||
AssignTicketToSprint, AttachLiveAgent, AuthenticateSession, BackgroundCommandArchive,
|
||||
CancelBackgroundTask, ChangeAgentProfile, CheckEmbedderSuggestion,
|
||||
CloneOpenCodeProfileFromSeed, CloseProject, CloseTab, CloseTerminal, CloseTicketAssistant,
|
||||
ConfigureProfiles, ContextGuardUseCases, CreateAgentFromScratch, CreateAgentFromTemplate,
|
||||
CreateIssue, CreateLayout, CreateMemory, CreateProject, CreateSkill, CreateSprint,
|
||||
CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteIssue, DeleteLayout, DeleteMemory,
|
||||
DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint, DeleteTemplate,
|
||||
DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles, DismissEmbedderSuggestion,
|
||||
EnsureLocalModelServer, FirstRunState, GetLiveStateLean, GetMemory, GetProjectPermissions,
|
||||
GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage,
|
||||
GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase, InspectConversation, LaunchAgent,
|
||||
LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput, ListDevices, ListEmbedderProfiles,
|
||||
ListIssues, ListLayouts, ListMemories, ListModelServers, ListProfiles, ListProjects,
|
||||
ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions,
|
||||
LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime,
|
||||
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
|
||||
OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
|
||||
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
|
||||
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMemory, ReadMemoryIndex,
|
||||
ReadProjectContext, ReadSkill, RecallMemory, ReconcileLayouts, ReconcileLiveState,
|
||||
ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameDevice,
|
||||
RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal, ResolveAgentPermissions,
|
||||
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, RotateConversationLog,
|
||||
SaveEmbedderProfile, SaveModelServer, SaveProfile, SessionLimitService, SetActiveLayout,
|
||||
SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand, StopLiveAgent,
|
||||
StructuredRoutingMode, StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate,
|
||||
TerminalSessions, UnassignSkillFromAgent, UnassignTicketFromSprint, UnlinkIssues,
|
||||
UpdateAgentContext, UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState,
|
||||
UpdateMemory, UpdateProjectContext, UpdateProjectPermissions, UpdateSkill, UpdateTemplate,
|
||||
WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
||||
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, RevokeAllDevices, RevokeDevice,
|
||||
RotateConversationLog, SaveEmbedderProfile, SaveModelServer, SaveProfile, SessionLimitService,
|
||||
SetActiveLayout, SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand,
|
||||
StopLiveAgent, StructuredRoutingMode, StructuredSessions, SuggestedThisSession,
|
||||
SyncAgentWithTemplate, TerminalSessions, TouchDevice, UnassignSkillFromAgent,
|
||||
UnassignTicketFromSprint, UnlinkIssues, UpdateAgentContext, UpdateAgentPermissions,
|
||||
UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
|
||||
UpdateProjectPermissions, UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory,
|
||||
WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, AgentSession, AgentSessionFactory, AgentToolPolicyStore,
|
||||
AgentWakePort, AssistantContextProvider, BackgroundTaskPortError, BackgroundTaskRunner,
|
||||
BackgroundTaskStore, Clock, Embedder, EmbedderEnvInspector, EmbedderProfileStore,
|
||||
EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator, IssueNumberAllocator,
|
||||
IssueStore, MemoryRecall, MemoryStore, PermissionStore, ProcessSpawner, ProfileStore,
|
||||
ProjectStore, PtyPort, ScheduledTask, Scheduler, SkillStore, SprintStore,
|
||||
BackgroundTaskStore, Clock, DeviceSessionStore, Embedder, EmbedderEnvInspector,
|
||||
EmbedderProfileStore, EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator,
|
||||
IssueNumberAllocator, IssueStore, MemoryRecall, MemoryStore, PermissionStore, ProcessSpawner,
|
||||
ProfileStore, ProjectStore, PtyPort, ScheduledTask, Scheduler, SkillStore, SprintStore,
|
||||
StructuredSessionEnvironmentPreparer, TemplateStore, ToolInvoker, WakeError, WakeReason,
|
||||
WindowStateStore,
|
||||
};
|
||||
@ -69,14 +72,15 @@ use infrastructure::{
|
||||
embedder_from_profile, AdaptiveMemoryRecall, BackgroundCompletionSink,
|
||||
BackgroundTaskReadyToDeliver, ClaudePermissionProjector, ClaudeTranscriptInspector,
|
||||
CliAgentRuntime, CodexPermissionProjector, CommandBackgroundRunner, EmbedderEnvProbe,
|
||||
FsAssistantContextStore, FsBackgroundTaskStore, FsConversationLog, FsEmbedderProfileStore,
|
||||
FsEmbedderPromptStore, FsHandoffStore, FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore,
|
||||
FsMemoryStore, FsModelServerRegistry, FsOrchestratorWatcher, FsPermissionStore, FsProfileStore,
|
||||
FsProjectStore, FsProviderSessionStore, FsSkillStore, FsSprintStore, FsTemplateStore,
|
||||
FsWindowStateStore, Git2Repository, HeuristicHandoffSummarizer, HfModelArtifactDownloader,
|
||||
HttpOpenAiCompatibleProbe, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
|
||||
LlamaCppRuntime, LocalFileSystem, LocalManagedProcess, LocalProcessSpawner, McpServer,
|
||||
MediatedInbox, NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter, RwFileGuard,
|
||||
FsAssistantContextStore, FsBackgroundTaskStore, FsConversationLog, FsDeviceSessionStore,
|
||||
FsEmbedderProfileStore, FsEmbedderPromptStore, FsHandoffStore, FsIssueNumberAllocator,
|
||||
FsIssueStore, FsLiveStateStore, FsMemoryStore, FsModelServerRegistry, FsOrchestratorWatcher,
|
||||
FsPermissionStore, FsProfileStore, FsProjectStore, FsProviderSessionStore, FsSkillStore,
|
||||
FsSprintStore, FsTemplateStore, FsWindowStateStore, Git2Repository, HeuristicHandoffSummarizer,
|
||||
HfModelArtifactDownloader, HttpOpenAiCompatibleProbe, IdeaiContextStore,
|
||||
InMemoryConversationRegistry, InMemoryMailbox, InMemoryPairAttemptLimiter, LlamaCppRuntime,
|
||||
LocalFileSystem, LocalManagedProcess, LocalProcessSpawner, McpServer, MediatedInbox,
|
||||
NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter, RwFileGuard,
|
||||
StructuredSessionFactory, SystemClock, SystemMillisClock, TicketAssistantEnvironmentPreparer,
|
||||
TicketToolProvider, TokioBroadcastEventBus, TokioScheduler, ToolPolicyRegistry, UuidGenerator,
|
||||
VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS,
|
||||
@ -798,6 +802,24 @@ impl AgentResumer for AppAgentResumer {
|
||||
pub struct BackendCore {
|
||||
/// Trivial health use case validating the end-to-end wiring.
|
||||
pub health: Arc<HealthUseCase>,
|
||||
/// Pair a persistent device session.
|
||||
pub pair_device: Arc<PairDevice>,
|
||||
/// Limits failed pairing attempts.
|
||||
pub pair_attempt_limiter: Arc<dyn PairAttemptLimiter>,
|
||||
/// Authenticate a persistent device session.
|
||||
pub authenticate_session: Arc<AuthenticateSession>,
|
||||
/// Throttled update of the authenticated device last-seen timestamp.
|
||||
pub touch_device: Arc<TouchDevice>,
|
||||
/// List paired devices.
|
||||
pub list_devices: Arc<ListDevices>,
|
||||
/// Rename a paired device.
|
||||
pub rename_device: Arc<RenameDevice>,
|
||||
/// Revoke one paired device.
|
||||
pub revoke_device: Arc<RevokeDevice>,
|
||||
/// Revoke every paired device.
|
||||
pub revoke_all_devices: Arc<RevokeAllDevices>,
|
||||
/// Paired-device store port, exposed for legacy logout revocation.
|
||||
pub device_session_store: Arc<dyn DeviceSessionStore>,
|
||||
/// Create a project (init `.ideai/`, register it).
|
||||
pub create_project: Arc<CreateProject>,
|
||||
/// Open a project (load meta + manifest).
|
||||
@ -1134,11 +1156,21 @@ impl BackendCore {
|
||||
Arc::clone(&fs) as Arc<dyn FileSystem>,
|
||||
app_data_dir.to_string_lossy().into_owned(),
|
||||
));
|
||||
let device_session_store = Arc::new(FsDeviceSessionStore::new(
|
||||
Arc::clone(&fs) as Arc<dyn FileSystem>,
|
||||
app_data_dir.to_string_lossy().into_owned(),
|
||||
));
|
||||
let pair_attempt_limiter = Arc::new(InMemoryPairAttemptLimiter::new(
|
||||
Arc::clone(&clock) as Arc<dyn Clock>
|
||||
));
|
||||
|
||||
// Port-typed handles for injection.
|
||||
let fs_port = Arc::clone(&fs) as Arc<dyn FileSystem>;
|
||||
let store_port = Arc::clone(&store) as Arc<dyn ProjectStore>;
|
||||
let window_state_port = Arc::clone(&window_state_store) as Arc<dyn WindowStateStore>;
|
||||
let device_session_port = Arc::clone(&device_session_store) as Arc<dyn DeviceSessionStore>;
|
||||
let pair_attempt_limiter_port =
|
||||
Arc::clone(&pair_attempt_limiter) as Arc<dyn PairAttemptLimiter>;
|
||||
let events_port = Arc::clone(&event_bus) as Arc<dyn EventBus>;
|
||||
|
||||
// --- Use cases (ports injected as Arc<dyn Port>) ---
|
||||
@ -1147,6 +1179,27 @@ impl BackendCore {
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let pair_device = Arc::new(PairDevice::new(
|
||||
Arc::clone(&device_session_port),
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
));
|
||||
let authenticate_session =
|
||||
Arc::new(AuthenticateSession::new(Arc::clone(&device_session_port)));
|
||||
let touch_device = Arc::new(TouchDevice::new(
|
||||
Arc::clone(&device_session_port),
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
));
|
||||
let list_devices = Arc::new(ListDevices::new(Arc::clone(&device_session_port)));
|
||||
let rename_device = Arc::new(RenameDevice::new(Arc::clone(&device_session_port)));
|
||||
let revoke_device = Arc::new(RevokeDevice::new(
|
||||
Arc::clone(&device_session_port),
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let revoke_all_devices = Arc::new(RevokeAllDevices::new(
|
||||
Arc::clone(&device_session_port),
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
|
||||
let create_project = Arc::new(CreateProject::new(
|
||||
Arc::clone(&store_port),
|
||||
@ -2316,6 +2369,15 @@ impl BackendCore {
|
||||
|
||||
Self {
|
||||
health,
|
||||
pair_device,
|
||||
pair_attempt_limiter: Arc::clone(&pair_attempt_limiter_port),
|
||||
authenticate_session,
|
||||
touch_device,
|
||||
list_devices,
|
||||
rename_device,
|
||||
revoke_device,
|
||||
revoke_all_devices,
|
||||
device_session_store: Arc::clone(&device_session_port),
|
||||
create_project,
|
||||
open_project,
|
||||
close_project,
|
||||
|
||||
Reference in New Issue
Block a user