From 86dba266a066ed3aec3efcc1df5725142c1a5fa8 Mon Sep 17 00:00:00 2001 From: rschumann Date: Wed, 8 Jul 2026 14:15:54 +0200 Subject: [PATCH 1/2] fix(shared-folders): backfill pods_provisioned for route-only sidecars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A schema-v3 progressive-sync sidecar (route-only, no pods_provisioned) reached /api/shared-folders/list as-is and blank-screened the Files and Settings sharing UIs, which map over the field unguarded (2026-07-08). normalize_shared_binding_value already backfills schema_version, slug, folder_id, sync_layout, targets, and runtime_status — pods_provisioned was the one UI-required field it skipped. Co-Authored-By: Claude Fable 5 --- tray/src/web_server.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tray/src/web_server.rs b/tray/src/web_server.rs index 8ec5138..53e0baa 100644 --- a/tray/src/web_server.rs +++ b/tray/src/web_server.rs @@ -12190,6 +12190,10 @@ fn normalize_shared_binding_value(binding: &mut serde_json::Value) { }); obj.entry("sync_layout".to_string()) .or_insert_with(|| serde_json::Value::String("legacy_bucket_root".to_string())); + // The UI contract types pods_provisioned as a required array; route-only + // sidecars (schema v3 progressive bindings) may omit it entirely. + obj.entry("pods_provisioned".to_string()) + .or_insert_with(|| serde_json::Value::Array(Vec::new())); let pods = obj .get("pods_provisioned") @@ -19092,6 +19096,41 @@ mod tests { assert!(claus.contains("HANDOFF.md")); } + #[test] + fn normalize_backfills_pods_provisioned_for_route_only_sidecar() { + // Regression: a schema-v3 progressive-sync sidecar carries only + // routes_provisioned; serving it without pods_provisioned crashed the + // Files and Settings sharing UIs (2026-07-08). + let mut binding = serde_json::json!({ + "schema_version": 3, + "bucket": "tytus-progressive-canary", + "slug": "p6canary", + "local_path": "/tmp/p6canary/", + "auto_sync": false, + "routes_provisioned": ["r0g3", "r1adv"], + }); + normalize_shared_binding_value(&mut binding); + assert_eq!( + binding.get("pods_provisioned"), + Some(&serde_json::json!([])), + "route-only sidecars must serialize pods_provisioned as an empty array" + ); + // Existing pod lists must pass through untouched. + let mut with_pods = serde_json::json!({ + "schema_version": 2, + "bucket": "shared", + "slug": "shared", + "local_path": "/tmp/shared/", + "auto_sync": true, + "pods_provisioned": ["wannolot-01"], + }); + normalize_shared_binding_value(&mut with_pods); + assert_eq!( + with_pods.get("pods_provisioned"), + Some(&serde_json::json!(["wannolot-01"])) + ); + } + #[test] fn shared_folder_direct_answer_fails_closed_when_selected_but_grant_missing() { let _grant_guard = shared_grant_test_serial_lock(); From d1ee100460409ee9afa86cbe9f5c44908c755e98 Mon Sep 17 00:00:00 2001 From: rschumann Date: Wed, 8 Jul 2026 14:26:33 +0200 Subject: [PATCH 2/2] fix(shared-folders): coerce null/non-array pods_provisioned as well Codex round: entry().or_insert_with only healed a missing key; a null or non-array value would still reach the UI. Coerce any non-array to []. Co-Authored-By: Claude Fable 5 --- tray/src/web_server.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/tray/src/web_server.rs b/tray/src/web_server.rs index 53e0baa..276ab22 100644 --- a/tray/src/web_server.rs +++ b/tray/src/web_server.rs @@ -12191,9 +12191,18 @@ fn normalize_shared_binding_value(binding: &mut serde_json::Value) { obj.entry("sync_layout".to_string()) .or_insert_with(|| serde_json::Value::String("legacy_bucket_root".to_string())); // The UI contract types pods_provisioned as a required array; route-only - // sidecars (schema v3 progressive bindings) may omit it entirely. - obj.entry("pods_provisioned".to_string()) - .or_insert_with(|| serde_json::Value::Array(Vec::new())); + // sidecars (schema v3 progressive bindings) may omit it entirely, and a + // hand-edited sidecar could carry null or a non-array. + let pods_is_array = obj + .get("pods_provisioned") + .map(|v| v.is_array()) + .unwrap_or(false); + if !pods_is_array { + obj.insert( + "pods_provisioned".to_string(), + serde_json::Value::Array(Vec::new()), + ); + } let pods = obj .get("pods_provisioned") @@ -19115,6 +19124,20 @@ mod tests { Some(&serde_json::json!([])), "route-only sidecars must serialize pods_provisioned as an empty array" ); + // Malformed values (null / non-array) are coerced, not passed through. + let mut with_null = serde_json::json!({ + "schema_version": 2, + "bucket": "shared", + "slug": "shared", + "local_path": "/tmp/shared/", + "auto_sync": true, + "pods_provisioned": serde_json::Value::Null, + }); + normalize_shared_binding_value(&mut with_null); + assert_eq!( + with_null.get("pods_provisioned"), + Some(&serde_json::json!([])) + ); // Existing pod lists must pass through untouched. let mut with_pods = serde_json::json!({ "schema_version": 2,