fix(desktop): preserve current workspace activation seams

Reconcile the multi-workspace agent stack with current main without dropping the authenticated-media redirect boundary, agent-managed profile setting, or shared-compute status publication. Keep the file-size exceptions explicit for the combined state and command wiring.

Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757
2026-07-17 21:07:58 -04:00
parent b94c6e9ed3
commit 2b72652096
3 changed files with 48 additions and 1 deletions
+5 -1
View File
@@ -374,7 +374,11 @@ const overrides = new Map([
// +5 (1068 -> 1073): merge with main, which independently added the
// managed_agent_profile_reconcile_enabled flag (field + doc + init) under
// its own 1042-line override. Union of two separately approved additions.
["src-tauri/src/app_state.rs", 1054],
["src-tauri/src/app_state.rs", 1090],
// lazy-workspace-activation adds seven command/state wiring lines to lib.rs.
// Keeping the registration and safety-gate setup together is clearer than a
// one-off extraction whose only purpose would be satisfying the line cap.
["src-tauri/src/lib.rs", 1007],
// multi-slot splitting + no-op suppression (#1309): the ReadStateManager
// class grew from ~700 lines to ~1019 with the addition of
// splitContextsIntoBudgetedSlots (pure fn + 5 tests), publishSplitSlots,
+35
View File
@@ -18,6 +18,15 @@ use crate::managed_agents::ManagedAgentProcess;
pub struct AppState {
pub keys: Mutex<Keys>,
pub http_client: reqwest::Client,
/// A no-redirect client for authenticated relay media fetches (download,
/// clipboard copy, snapshot, editor). Every caller pre-validates the URL
/// origin, but the app-wide `http_client` follows redirects by default, so
/// a relay `/media/` URL returning a 3xx to an off-origin or private host
/// would forward the minted media Authorization header across origins —
/// a redirect-hop SSRF. This client treats any 3xx as a non-success
/// response (surfaced as an error) so the auth token never leaves the
/// validated relay origin.
pub media_fetch_client: reqwest::Client,
/// Workspace-provided relay URL override. Set by `apply_workspace` on app
/// init and takes priority over env vars and compile-time defaults.
pub relay_url_override: Mutex<Option<String>>,
@@ -155,6 +164,27 @@ fn identity_from_env() -> Option<Keys> {
}
}
/// Build the no-redirect HTTP client used for authenticated relay media
/// fetches (download / copy).
///
/// This client is a security boundary, not a convenience: it carries a minted
/// media `Authorization` header, so it MUST NOT follow redirects. A relay 3xx
/// to an off-origin or private host would otherwise forward that header across
/// origins (a redirect-hop SSRF). `redirect::Policy::none()` returns the 3xx
/// verbatim so the caller can reject it.
///
/// Returned as a `Result` so the fail-closed invariant is testable — callers
/// must never substitute a redirect-following client on build failure. Shares
/// the localhost `resolve`/pool config with the app-wide `http_client`.
pub fn build_media_fetch_client() -> reqwest::Result<reqwest::Client> {
reqwest::Client::builder()
.resolve("localhost", std::net::SocketAddr::from(([127, 0, 0, 1], 0)))
.pool_idle_timeout(std::time::Duration::from_secs(10))
.pool_max_idle_per_host(1)
.redirect(reqwest::redirect::Policy::none())
.build()
}
pub fn build_app_state() -> AppState {
// Env var takes precedence (dev/CI). If absent, resolve_persisted_identity()
// in setup() will replace the ephemeral placeholder with a persisted key.
@@ -177,6 +207,11 @@ pub fn build_app_state() -> AppState {
.pool_max_idle_per_host(1)
.build()
.unwrap_or_else(|_| reqwest::Client::new()),
media_fetch_client: build_media_fetch_client().expect(
"media_fetch_client must build with redirect::Policy::none(); a \
redirect-following fallback would forward the minted media auth \
header across origins (redirect-hop SSRF)",
),
relay_url_override: Mutex::new(None),
managed_agent_restore_pending: AtomicBool::new(false),
managed_agent_profile_reconcile_enabled: AtomicBool::new(true),
@@ -105,6 +105,7 @@ pub async fn apply_workspace(
relay_url: String,
nsec: Option<String>,
repos_dir: Option<String>,
agent_managed_profiles: Option<bool>,
app: AppHandle,
) -> Result<(), String> {
let restore_app = app.clone();
@@ -151,6 +152,13 @@ pub async fn apply_workspace(
*keys_guard = keys;
}
// Keep the backend-side reconcile guard aligned with the frontend
// experiment before workspace activation can spawn any agents. Missing
// means the stable behavior: desktop remains authoritative.
state
.managed_agent_profile_reconcile_enabled
.store(!agent_managed_profiles.unwrap_or(false), Ordering::Release);
// ── One-shot legacy migration (non-fatal) ─────────────────────────────
// Pin any blank-relay agent record to the first workspace applied
// after boot — exactly what blank would have resolved to at boot