field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 22aafe19bd86c4eefca2cda2ecc0490668114034
parent 9b012edb3aa847dfa04e2d23a38d8cb3f551c307
Author: triesap <tyson@radroots.org>
Date:   Sat, 19 Sep 2026 18:46:00 +0000

media: select only enforceable uploader policies

- Default unqualified adapters to the existing hardened foreground uploader
- Preserve frozen capture and uncertain media through cancellation and offline failure
- Verify default policy selection without native enqueue or hidden authorization
- Regenerate exact native artifacts and retain full verification evidence

Diffstat:
MTera/Runtime/TeraAddMediaCoordinator.swift | 4+++-
MTeraFFI/provenance.json | 14+++++++-------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 8++++----
MTeraFFI/source/aarch64-apple-ios-sim.json | 8++++----
MTeraFFI/source/aarch64-apple-ios.json | 8++++----
MTeraTests/TeraForegroundSubmissionTests.swift | 44+++++++++++++++++++++++++++++++++++++++++---
Mcore/crates/tera_core/src/runtime/product_surface/submission/media_foreground_tests.rs | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mrelease/provenance.json | 4++--
9 files changed, 132 insertions(+), 30 deletions(-)

diff --git a/Tera/Runtime/TeraAddMediaCoordinator.swift b/Tera/Runtime/TeraAddMediaCoordinator.swift @@ -42,7 +42,9 @@ extension TeraAddMediaHandling { } func prefersSharedForegroundUpload(ownerID _: String) async throws -> Bool { - false + // A conformer must explicitly establish native capability before selecting it. + // The shared uploader retains the required destination enforcement by default. + true } func uploadInBackground(job: TeraNativeUploadJob, media: TeraPreparedMedia) async throws -> TeraAddBackgroundUploadReceipt { diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -94,17 +94,17 @@ { "bytes": 100688, "path": "source/aarch64-apple-darwin.json", - "sha256": "0bcd57e3c345307f2fc9a4f84e65391755e3acc4bb0fe208380ec9ed39295523" + "sha256": "f97e05d5b587358e94a938b7c74591970a11392cd5a8c87c4d7d12b934629058" }, { "bytes": 100532, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "30973a056e3e260e8296a34a3c357cc2686580a361058630ed617209f5899c38" + "sha256": "6811dca32ae3685b2f7e0121cc2f8e0dfa2401a6826d6ad40b510dd754353677" }, { "bytes": 100528, "path": "source/aarch64-apple-ios.json", - "sha256": "154fe2a72d0523d9fb1bf3077231946fd1de5548c20080f5653a3f276f75db9d" + "sha256": "698ed0982a5d5347d757d7f3dce2ac1f12ea16ad03b92d43f6867e7696b8c73a" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "6eaee343a67b3d16bfc623ff9aadfb9ceb68186f" + "tree": "f1bd4bd0656de8a67a9602889c0655fb3bdadc56" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -176,17 +176,17 @@ { "bytes": 100688, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "0bcd57e3c345307f2fc9a4f84e65391755e3acc4bb0fe208380ec9ed39295523" + "sha256": "f97e05d5b587358e94a938b7c74591970a11392cd5a8c87c4d7d12b934629058" }, { "bytes": 100532, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "30973a056e3e260e8296a34a3c357cc2686580a361058630ed617209f5899c38" + "sha256": "6811dca32ae3685b2f7e0121cc2f8e0dfa2401a6826d6ad40b510dd754353677" }, { "bytes": 100528, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "154fe2a72d0523d9fb1bf3077231946fd1de5548c20080f5653a3f276f75db9d" + "sha256": "698ed0982a5d5347d757d7f3dce2ac1f12ea16ad03b92d43f6867e7696b8c73a" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "6eaee343a67b3d16bfc623ff9aadfb9ceb68186f" -manifest_sha256 = "e22296b2d30d4d3707b6cd8410c8b07d307aad1d8f3936584b2b51b1ffcadf8a" +source_tree = "f1bd4bd0656de8a67a9602889c0655fb3bdadc56" +manifest_sha256 = "6e9f9645c78446371d5fdfc90007ca89d09ab102cc17f417d03d7eaff08a9810" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -1041,10 +1041,10 @@ "sha256": "1e2880690f6723c9eedb3bf4742cb1609e7de1c6035459a8579feb61d2a021c7" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_foreground_tests.rs": { - "bytes": 7623, - "git_blob": "b614326b5ab7f72a78521de8c27fef0086edd7e1", + "bytes": 9867, + "git_blob": "79e6903546d9a4fc79075bb1d3e64a1313d68ecc", "mode": "100644", - "sha256": "6fbe6bcb7b086ea1a3dc90c404a08364cbb673d9b0059ebcf51db967534295a5" + "sha256": "12db26266ab018756a7bb722cb1e9bcb9c834346734625d03d0e2994e8228f09" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_recovery_tests.rs": { "bytes": 5308, @@ -2152,6 +2152,6 @@ } }, "policy": "staged_inputs", - "tree": "6eaee343a67b3d16bfc623ff9aadfb9ceb68186f" + "tree": "f1bd4bd0656de8a67a9602889c0655fb3bdadc56" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -1037,10 +1037,10 @@ "sha256": "1e2880690f6723c9eedb3bf4742cb1609e7de1c6035459a8579feb61d2a021c7" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_foreground_tests.rs": { - "bytes": 7623, - "git_blob": "b614326b5ab7f72a78521de8c27fef0086edd7e1", + "bytes": 9867, + "git_blob": "79e6903546d9a4fc79075bb1d3e64a1313d68ecc", "mode": "100644", - "sha256": "6fbe6bcb7b086ea1a3dc90c404a08364cbb673d9b0059ebcf51db967534295a5" + "sha256": "12db26266ab018756a7bb722cb1e9bcb9c834346734625d03d0e2994e8228f09" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_recovery_tests.rs": { "bytes": 5308, @@ -2148,6 +2148,6 @@ } }, "policy": "staged_inputs", - "tree": "6eaee343a67b3d16bfc623ff9aadfb9ceb68186f" + "tree": "f1bd4bd0656de8a67a9602889c0655fb3bdadc56" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -1037,10 +1037,10 @@ "sha256": "1e2880690f6723c9eedb3bf4742cb1609e7de1c6035459a8579feb61d2a021c7" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_foreground_tests.rs": { - "bytes": 7623, - "git_blob": "b614326b5ab7f72a78521de8c27fef0086edd7e1", + "bytes": 9867, + "git_blob": "79e6903546d9a4fc79075bb1d3e64a1313d68ecc", "mode": "100644", - "sha256": "6fbe6bcb7b086ea1a3dc90c404a08364cbb673d9b0059ebcf51db967534295a5" + "sha256": "12db26266ab018756a7bb722cb1e9bcb9c834346734625d03d0e2994e8228f09" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_recovery_tests.rs": { "bytes": 5308, @@ -2148,6 +2148,6 @@ } }, "policy": "staged_inputs", - "tree": "6eaee343a67b3d16bfc623ff9aadfb9ceb68186f" + "tree": "f1bd4bd0656de8a67a9602889c0655fb3bdadc56" } } diff --git a/TeraTests/TeraForegroundSubmissionTests.swift b/TeraTests/TeraForegroundSubmissionTests.swift @@ -5,15 +5,28 @@ import XCTest @MainActor final class TeraForegroundSubmissionTests: XCTestCase { func testViewCancellationRetainsForegroundResultAndFrozenCaptureWithoutNativeEnqueue() async throws { + try await exerciseForeground(useDefaultPolicy: false) + } + + func testAdapterWithoutQualifiedNativePolicyUsesSharedForegroundAndFrozenCapture() async throws { + try await exerciseForeground(useDefaultPolicy: true) + } + + private func exerciseForeground(useDefaultPolicy: Bool) async throws { let backend = AddBackend() let client = try await TeraAddStoreTests.startedClient(backend) - let media = AddMediaHarness(foreground: true) + let harness = AddMediaHarness(foreground: true) + let media: any TeraAddMediaHandling = if useDefaultPolicy { + DefaultPolicyMedia(harness: harness) + } else { + harness + } let store = TeraAddStore(runtimeClient: client, media: media) await store.configure(snapshot: backend.snapshot()) await store.start() store.selectType(.createPhotoUpdate) store.updateForm(\.content, "original foreground photo") - await store.updateForm(\.media, [media.captureImage()]) + await store.updateForm(\.media, [harness.captureImage()]) let pause = ResourceTestPause() await backend.submissionBackend.pauseForeground(pause) let submit = Task { await store.submit() } @@ -31,7 +44,7 @@ final class TeraForegroundSubmissionTests: XCTestCase { let foreground = await backend.submissionBackend.foregroundCount let native = await backend.submissionBackend.uploadCount let persisted = await backend.submissionBackend.completionPersisted - let settlements = await media.settlementValues() + let settlements = await harness.settlementValues() XCTAssertEqual(foreground, 1) XCTAssertEqual(native, 0) XCTAssertTrue(persisted) @@ -40,3 +53,28 @@ final class TeraForegroundSubmissionTests: XCTestCase { _ = try await client.stop() } } + +/// Deliberately omits a native policy override; its media operations are usable. +private struct DefaultPolicyMedia: TeraAddMediaHandling { + let harness: AddMediaHarness + + func confirmDurableComposerMedia(_ media: [TeraComposerMedia]) async throws { + try await (harness as any TeraAddMediaHandling).confirmDurableComposerMedia(media) + } + + func support() async throws -> TeraAddMediaSupport { + await harness.support() + } + + func importImages(limit: Int) async throws -> [TeraPreparedMedia] { + await harness.importImages(limit: limit) + } + + func captureImage() async throws -> TeraPreparedMedia { + await harness.captureImage() + } + + func open(_ media: [TeraPreparedMedia]) async throws -> TeraOpenedMedia { + try await harness.open(media) + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/media_foreground_tests.rs b/core/crates/tera_core/src/runtime/product_surface/submission/media_foreground_tests.rs @@ -1,6 +1,9 @@ use std::{sync::atomic::Ordering, time::Duration}; use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; +use radroots_sdk::transport::{ + BlossomConfig, BlossomEndpointAuthority, BlossomHostKind, BlossomProfile, +}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}, @@ -43,9 +46,6 @@ async fn foreground_upload_preserves_frozen_parent_and_verifies_exact_bytes_afte runtime.shutdown().await.unwrap(); drop(runtime); if sqlite { - use radroots_sdk::transport::{ - BlossomConfig, BlossomEndpointAuthority, BlossomHostKind, BlossomProfile, - }; let config = BlossomConfig::from_profile( BlossomProfile::new( BlossomHostKind::Simulator, @@ -129,6 +129,68 @@ async fn foreground_does_not_replace_an_unreconciled_native_attempt() { runtime.shutdown().await.unwrap(); } +#[tokio::test] +async fn offline_foreground_preserves_parent_without_hidden_replacement_or_publication() { + // Reserve an endpoint without a listener so another process cannot acquire it. + // A bounded fixture timeout covers platforms that drop rather than refuse SYNs. + let unavailable = tokio::net::TcpSocket::new_v4().unwrap(); + unavailable.bind("127.0.0.1:0".parse().unwrap()).unwrap(); + let origin = format!("http://{}", unavailable.local_addr().unwrap()); + let signer = CountingSigner::new(); + let config = BlossomConfig::from_profile( + BlossomProfile::new( + BlossomHostKind::Simulator, + BlossomEndpointAuthority::LoopbackDevelopment, + &origin, + std::iter::empty::<&str>(), + ) + .unwrap(), + ) + .with_network_policy( + Duration::from_millis(100), + Duration::from_secs(1), + 1, + Duration::from_millis(1), + ) + .unwrap(); + let runtime = runtime_with_blossom(None, signer.clone(), "ws://127.0.0.1:19999", config).await; + let request = request(); + prepare(&runtime, &request, true).await; + let original = runtime.submission_operation_status(&request).await.unwrap(); + assert!( + runtime + .submission_upload_media(upload(&request, 1)) + .await + .is_err() + ); + let paused = runtime.submission_operation_status(&request).await.unwrap(); + assert_eq!(paused.captured(), original.captured()); + assert_eq!(paused.push(), original.push()); + assert_eq!(paused.receipt(), original.receipt()); + assert_eq!(paused.media()[0].stage(), Phase1MediaStage::Failed); + assert!(paused.media()[0].orphan().is_some()); + assert_eq!(*signer.kinds.lock().unwrap(), [24242]); + let revision = paused.intent().revision().get(); + assert!( + runtime + .submission_upload_media(upload(&request, revision)) + .await + .is_err() + ); + assert!( + runtime + .submission_prepare_native_upload(upload(&request, revision)) + .await + .is_err() + ); + assert_eq!(signer.count(), 1); + assert_eq!( + runtime.submission_operation_status(&request).await.unwrap(), + paused + ); + runtime.shutdown().await.unwrap(); +} + async fn foreground_server() -> (String, tokio::task::JoinHandle<Vec<String>>) { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let origin = format!("http://{}", listener.local_addr().unwrap()); diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "7b3f92a57ebcc7873349403243098d5dcf993b2dbdab9ed84fea4a50c20baab0", - "ffi_provenance_sha256": "e22296b2d30d4d3707b6cd8410c8b07d307aad1d8f3936584b2b51b1ffcadf8a", + "ffi_provenance_sha256": "6e9f9645c78446371d5fdfc90007ca89d09ab102cc17f417d03d7eaff08a9810", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "b202075f3c34a3a70e8745ca6d6acab6c2cf5e2d6de9a2b2647475d73a8d3f28", @@ -22,7 +22,7 @@ "lib_revision": "963dd8dc99fc5f16ef7d7eea11f8d964770a696e", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "d5c9240384b50f006ee6e9eff886a3775546280e90a05cbe781eaa296632da26", - "tera_ffi_source_tree": "6eaee343a67b3d16bfc623ff9aadfb9ceb68186f", + "tera_ffi_source_tree": "f1bd4bd0656de8a67a9602889c0655fb3bdadc56", "xcode_package_lock_sha256": "b6806f710734f8e628d7a7d7b97826d89eea78fba32fa9b464f513bcadd411e2" }, "version": "0.1.0-alpha"