From 66ef45111549f6c93edf854cf63d92adc72d99e5 Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Thu, 16 Jul 2026 10:56:22 -0500 Subject: [PATCH] Seed predefined rules at startup without requiring Warp auth Previously, predefined rules (11 system-defined behavioral rules) were only seeded when the user navigated to the Rules settings page, and required a cloud-authenticated owner to create cloud objects. Since Galaxy operates without Warp cloud auth: 1. personal_drive() now returns a synthetic local owner ('local-galaxy-user') when not authenticated, allowing cloud objects (rules, etc.) to be created and stored locally in SQLite. 2. seed_predefined_rules_if_needed() runs at app startup (after CloudModel and UpdateManager are initialized) to seed rules on first launch without requiring the user to visit settings. Rules are now visible in Galaxy Drive > Rules on first launch. --- app/src/lib.rs | 63 +++++++++++++++++++++++++++ app/src/workspaces/user_workspaces.rs | 16 +++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/app/src/lib.rs b/app/src/lib.rs index 1569e3e5..0ff54b17 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -2205,6 +2205,10 @@ pub(crate) fn initialize_app( ctx.add_singleton_model(HarnessAvailabilityModel::new); ctx.add_singleton_model(ConnectedSelfHostedWorkersModel::new); + // Seed predefined rules on first launch if no global rules exist. + // This runs after CloudModel and UpdateManager are initialized. + seed_predefined_rules_if_needed(ctx); + let tip_model_handle = ctx.add_singleton_model(|ctx| { ai::agent_tips::AITipModel::::new_for_agent_tips(ctx) }); @@ -2913,6 +2917,65 @@ fn launch(ctx: &mut galaxyui::AppContext, app_state: Option, launch_mo /// /// Additionally, we must not write anything to stdout in this function, as it /// can interfere with test harnesses collecting the set of tests to run. (This +/// Seeds predefined system rules into the local CloudModel on first launch +/// (when no global rules exist and they haven't been seeded before). +/// This ensures rules are available for AI requests without requiring the user +/// to manually visit the Rules settings page. +fn seed_predefined_rules_if_needed(ctx: &mut AppContext) { + use ai::facts::predefined_rules::PREDEFINED_RULES; + use ai::facts::{AIFact, AIMemory}; + use cloud_object::model::generic_string_model::GenericStringObjectId; + use cloud_object::model::persistence::CloudModel; + use server::cloud_objects::update_manager::UpdateManager; + use server::ids::ClientId; + use workspaces::user_workspaces::UserWorkspaces; + + let settings = AISettings::as_ref(ctx); + if settings.has_seeded_predefined_rules() { + return; + } + + // Check if any rules already exist + let has_existing_rules = CloudModel::handle(ctx) + .as_ref(ctx) + .get_all_objects_of_type::() + .next() + .is_some(); + + if has_existing_rules { + // Rules exist (e.g. from a previous session) — mark as seeded and skip + AISettings::handle(ctx).update(ctx, |settings, ctx| { + settings.mark_predefined_rules_seeded(ctx); + }); + return; + } + + let Some(owner) = UserWorkspaces::as_ref(ctx).personal_drive(ctx) else { + return; + }; + + log::info!( + "[rules] Seeding {} predefined rules on first launch", + PREDEFINED_RULES.len() + ); + + UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| { + for rule in PREDEFINED_RULES { + let ai_fact = AIFact::Memory(AIMemory { + is_autogenerated: false, + name: Some(rule.name.to_string()), + content: rule.content.to_string(), + suggested_logging_id: None, + }); + update_manager.create_ai_fact(ai_fact, ClientId::default(), owner, ctx); + } + }); + + AISettings::handle(ctx).update(ctx, |settings, ctx| { + settings.mark_predefined_rules_seeded(ctx); + }); +} + /// is why we're not simply calling the init() function above.) #[ctor::ctor] #[cfg(test)] diff --git a/app/src/workspaces/user_workspaces.rs b/app/src/workspaces/user_workspaces.rs index 24b6d47b..8fcb3e42 100644 --- a/app/src/workspaces/user_workspaces.rs +++ b/app/src/workspaces/user_workspaces.rs @@ -717,10 +717,18 @@ impl UserWorkspaces { // Returns the [`Owner`] for the user's personal drive. If the user is not authenticated, this // returns `None`. pub fn personal_drive(&self, ctx: &AppContext) -> Option { - AuthStateProvider::as_ref(ctx) - .get() - .user_id() - .map(|user_uid| Owner::User { user_uid }) + // Return the authenticated user's ID if available, otherwise provide a + // synthetic local owner so cloud objects (rules, etc.) can be created and + // stored locally without requiring Warp authentication. + Some( + AuthStateProvider::as_ref(ctx) + .get() + .user_id() + .map(|user_uid| Owner::User { user_uid }) + .unwrap_or_else(|| Owner::User { + user_uid: UserUid::new("local-galaxy-user"), + }), + ) } // Maps a [`Space`] into an [`Owner`], based on the user's team memberships. If the space