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:
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"