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.
This commit is contained in:
Ryan Ward
2026-07-16 10:56:22 -05:00
parent 5a5977d35d
commit 66ef451115
2 changed files with 75 additions and 4 deletions
+63
View File
@@ -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::<ai::AgentTip>::new_for_agent_tips(ctx)
});
@@ -2913,6 +2917,65 @@ fn launch(ctx: &mut galaxyui::AppContext, app_state: Option<AppState>, 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::<GenericStringObjectId, ai::facts::CloudAIFactModel>()
.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)]
+12 -4
View File
@@ -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<Owner> {
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