From ee3431dc50b974ea7a4ad4752f07682e092024cc Mon Sep 17 00:00:00 2001 From: lamemustafa Date: Wed, 22 Jul 2026 08:14:14 +0530 Subject: [PATCH 1/5] feat(tally): add local write fixture enrollment --- .../compatibility/compatibility-matrix.json | 2 +- .../compatibility/compatibility-surface.json | 18 +- .../synthetic-write-canary-fixture.md | 29 ++ src-tauri/src/commands.rs | 209 ++++++++++- .../0013_tally_write_fixture_enrollment.sql | 85 +++++ src-tauri/src/db/tally_mirror.rs | 351 +++++++++++++++++- src-tauri/src/lib.rs | 3 + src/main.tsx | 207 ++++++++++- 8 files changed, 895 insertions(+), 9 deletions(-) create mode 100644 docs/tally/compatibility/synthetic-write-canary-fixture.md create mode 100644 src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index 79c87e4..b853f50 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "0837a04c50b2f9ece55777c99a704137b1f781c28040bd9bb558a96d52a30b99", + "compatibility_surface_sha256": "616fdb4806bd4e78cfd184ac58a6b2faab53e60a9ca943e411124a39be276ffc", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index ecc4252..0547d16 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -57,6 +57,10 @@ "path": "docs/tally/compatibility/native-outstandings-ui-before.example.json", "sha256": "9f842721c5ac86c44c900d0c94b2e5342ff4928ea6dbbf2dee6dcc26a255a9e0" }, + { + "path": "docs/tally/compatibility/synthetic-write-canary-fixture.md", + "sha256": "95bae833d5a81e06a9cc0f054772820ec05acdf95033173b68cd01b4af00f206" + }, { "path": "docs/tally/support-matrix.md", "sha256": "d2acf62109c337299168555b447e9e8e70c12f22836e949fe6251ece62eb6a21" @@ -239,7 +243,7 @@ }, { "path": "src-tauri/src/commands.rs", - "sha256": "dc3992dfab507e095c56239ad480aa111c671c0b540f98981803bcb6ab065417" + "sha256": "123ba26cff15d62a2b2c7aef0cb07ecb1ed78a86ec4ed0b96cdc825e62d324bf" }, { "path": "src-tauri/src/db/encrypted.rs", @@ -269,13 +273,17 @@ "path": "src-tauri/src/db/migrations/0012_tally_window_terminal_evidence.sql", "sha256": "3649b141a5d5af81a00a8561bac3bae2ccaedb3b3c4a91cf187c799c49794753" }, + { + "path": "src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql", + "sha256": "8e63b553b85d59bb10b149eb2b3497504e4e443690228cb2975ce325d9e84168" + }, { "path": "src-tauri/src/db/tally_incremental.rs", "sha256": "bac2c859de102cf1e558f669dee0445a9406e95ee9923b6f52bd498ee768e73b" }, { "path": "src-tauri/src/db/tally_mirror.rs", - "sha256": "07bbfb8b2d46588ca27b1bf1da2cab1c1c75aab5ecffa3909bbea914f8a047d9" + "sha256": "f6aaa9d7bb1e3ba9a8aab32049a4d3210eec0e6f8034c5071ad7d33916be88fa" }, { "path": "src-tauri/src/db/tally_write_store.rs", @@ -283,7 +291,7 @@ }, { "path": "src-tauri/src/lib.rs", - "sha256": "07aa3cd8152c347be13d409c40780e3b9acd9f18cd0de68d76e86480a172abf2" + "sha256": "9942f21406efda26c683d0056eac96bef9e38bff0c30f4f1487dfb304c8108a2" }, { "path": "src-tauri/src/sync/coordinator.rs", @@ -335,7 +343,7 @@ }, { "path": "src/main.tsx", - "sha256": "c33c4d47c78dcbcc79c0406a8b83b5c35fda9a979c0c58146cd4a66b8e56dfa7" + "sha256": "c463fdca1e73a3f4a30a1ddc7f039dc87ba9f6f4c87f8334c94f2ffe750a6223" }, { "path": "src/styles.css", @@ -346,5 +354,5 @@ "sha256": "5a5c6eaaba234c3cbda52dfa040ed3314f79e535f744b87bc14d8d76a2299811" } ], - "manifest_sha256": "0837a04c50b2f9ece55777c99a704137b1f781c28040bd9bb558a96d52a30b99" + "manifest_sha256": "616fdb4806bd4e78cfd184ac58a6b2faab53e60a9ca943e411124a39be276ffc" } diff --git a/docs/tally/compatibility/synthetic-write-canary-fixture.md b/docs/tally/compatibility/synthetic-write-canary-fixture.md new file mode 100644 index 0000000..bce335b --- /dev/null +++ b/docs/tally/compatibility/synthetic-write-canary-fixture.md @@ -0,0 +1,29 @@ +# Synthetic write-canary fixture gate + +This gate records a local, revocable operator attestation for a future synthetic +write-canary. It does not construct XML, call Tally, write to Tally, change the +Capability Passport, or establish write support. + +## Enrollment prerequisites + +1. Use a dedicated disposable synthetic company. An existing demo company is + not automatically eligible: the operator attestation is a gate, not proof + that the company is disposable. +2. Do not use customer, personal, or production data. +3. Before any later canary, create an offline backup, record how to restore it, + and verify the restore path against a separate copy. If this is not possible, + do not acknowledge the backup guidance and do not proceed. +4. Persist the selected GUID-bearing company scope, then obtain a separate fresh + Probe review for the local enrollment. A review consumed by setup save cannot + be reused. + +## Local effects and revocation + +Enrollment stores only commitment hashes, attestation flags, and local event +timestamps. It does not store fixture content, company names, GUIDs, backup +locations, or free text in the enrollment evidence tables. The UI must continue +to report `write capability: Unknown`. + +Revocation appends a local `operator_revoked` event. It changes the local +candidate gate only and never alters Tally. A revoked fixture requires a new +fresh review and a new complete attestation before it can be enrolled again. diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index a9dc6f4..02ce2ea 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -6,7 +6,7 @@ use crate::db::tally_mirror::{ LocalReconciliationMismatch, ProofSummary, RedactedProofExport, ReviewedSetupInput, SelectedReadObservationCommitmentMaterial, SelectedReadObservationInput, SelectedReadScopeCommitmentMaterial, SelectedReadScopeInput, SourceIdentityInput, - TallyMirrorRepository, + TallyMirrorRepository, WriteFixtureEnrollmentInput, WriteFixtureEnrollmentStatus, }; use crate::gst::{GstDraftRequest, GstReturnDraft}; use crate::sync::coordinator::{SnapshotCoordinator, SnapshotJobStatus}; @@ -782,6 +782,32 @@ pub struct SavedTallySetup { pub review_cleanup_warning: Option<&'static str>, } +#[derive(Debug, Deserialize)] +pub struct EnrollTallyWriteFixtureRequest { + pub config: TallyConfig, + pub expected_review_id: String, + pub expected_review_commitment_sha256: String, + pub mirror_company_id: String, + pub selected_company_guid: String, + pub disposable_company_attested: bool, + pub no_customer_data_attested: bool, + pub backup_guidance_acknowledged: bool, +} + +#[derive(Debug, Deserialize)] +pub struct TallyWriteFixtureCompanyRequest { + pub mirror_company_id: String, +} + +#[derive(Debug, Serialize)] +pub struct TallyWriteFixtureEnrollmentResponse { + #[serde(flatten)] + pub status: WriteFixtureEnrollmentStatus, + pub tally_requests_attempted: u8, + pub tally_writes_attempted: u8, + pub review_cleanup_warning: Option<&'static str>, +} + #[tauri::command] pub async fn save_tally_setup( request: SaveTallySetupRequest, @@ -1022,6 +1048,187 @@ fn reconcile_review_cleanup( } } +#[tauri::command] +pub async fn enroll_tally_write_fixture( + request: EnrollTallyWriteFixtureRequest, + mirror: State<'_, TallyMirrorRepository>, + runtime: State<'_, TallyRuntime>, +) -> Result { + let canonical_origin = EndpointKey::from_config(&request.config) + .map(|endpoint| endpoint.as_str().to_string()) + .map_err(|_| { + tally_command_error( + "endpoint_configuration_invalid", + "Endpoint configuration", + "Tally endpoint validation failed", + "after_change", + false, + "Use the reviewed loopback endpoint and probe again.", + ) + })?; + let mut reservation = runtime + .reserve_cached_probe_fresh( + &request.config, + &request.expected_review_id, + SETUP_PROBE_MAX_AGE_MS, + ) + .map_err(tally_runtime_command_error)? + .ok_or_else(|| { + tally_command_error( + "reviewed_probe_expired", + "Operation", + "The reviewed Capability Passport is missing or older than five minutes.", + "safe", + false, + "Probe again, review the exact Passport and company scope, then enroll.", + ) + })?; + let observed_at_unix_ms = reservation.observed_at_unix_ms(); + let probe = reservation.result().clone(); + let result: Result = async { + if request.expected_review_commitment_sha256 + != reviewed_probe_commitment_sha256( + &request.expected_review_id, &canonical_origin, observed_at_unix_ms, &probe, + ).map_err(|_| tally_command_error( + "reviewed_probe_commitment_failed", "Operation", + "The cached endpoint, Passport, and company scope could not be verified.", + "safe", false, "Probe again before enrolling a fixture.", + ))? + { + return Err(tally_command_error( + "reviewed_probe_changed", "Operation", + "The reviewed Capability Passport no longer matches the cached probe.", + "safe", false, "Probe again and review the replacement Passport before enrolling.", + )); + } + let selected_guid = normalize_company_guid(&request.selected_company_guid).map_err(|_| { + tally_command_error( + "stable_company_identity_required", "Tally application", + "The selected company does not have an observed stable GUID.", + "after_change", false, "Select a GUID-bearing company from the current probe.", + ) + })?; + let matching_companies = probe.companies.iter().filter(|company| { + company.guid.as_deref().is_some_and(|guid| guid.eq_ignore_ascii_case(&selected_guid)) + }).count(); + if matching_companies != 1 { + return Err(tally_command_error( + if matching_companies == 0 { "reviewed_company_scope_changed" } else { "company_identity_ambiguous" }, + "Tally application", + "The selected company identity is not uniquely present in the reviewed probe.", + "safe", false, "Probe again and select one GUID-bearing company from the current result.", + )); + } + if probe.profile.features.get(&CapabilityFeatureId::Write) + .is_some_and(|evidence| evidence.state == CapabilityState::Unsupported) + { + return Err(tally_command_error( + "write_capability_unsupported", "Tally application", + "The reviewed Passport marks Tally write capability unsupported.", + "safe", false, "Do not enroll this scope for a write canary.", + )); + } + let pin = mirror.snapshot_source_pin(&request.mirror_company_id).await.map_err(|_| { + tally_command_error( + "persisted_company_scope_required", "Operation", + "A persisted observed company pin is required before fixture enrollment.", + "safe", false, "Save the reviewed company scope, then probe and enroll while it is fresh.", + ) + })?; + if pin.canonical_origin != canonical_origin || !pin.company_guid.eq_ignore_ascii_case(&selected_guid) { + return Err(tally_command_error( + "persisted_company_scope_changed", "Tally application", + "The persisted company pin does not match the fresh reviewed company identity.", + "safe", false, "Probe again and save the selected company scope before enrolling.", + )); + } + let enrollment = mirror.enroll_write_fixture(WriteFixtureEnrollmentInput { + company_id: request.mirror_company_id.clone(), + review_commitment_sha256: request.expected_review_commitment_sha256.clone(), + disposable_company_attested: request.disposable_company_attested, + no_customer_data_attested: request.no_customer_data_attested, + backup_guidance_acknowledged: request.backup_guidance_acknowledged, + enrolled_at_unix_ms: chrono::Utc::now().timestamp_millis(), + }).await.map_err(|_| tally_command_error( + "fixture_enrollment_store_failed", "Operation", + "The local write-fixture enrollment could not be stored.", + "safe", false, "Verify the three confirmations and local encrypted storage, then retry the fresh review.", + ))?; + let status = mirror.write_fixture_enrollment_status(&request.mirror_company_id).await.map_err(|_| { + tally_command_error("fixture_enrollment_status_unavailable", "Operation", "The local fixture status could not be read after enrollment.", "after_change", true, "Restart Bridge and inspect the local fixture status before any future canary.") + })?; + debug_assert!(!enrollment.id.is_empty()); + Ok(TallyWriteFixtureEnrollmentResponse { + status, + tally_requests_attempted: 0, + tally_writes_attempted: 0, + review_cleanup_warning: None, + }) + }.await; + let cleanup_succeeded = if result.is_ok() { + reservation.consume().unwrap_or(false) + } else { + reservation.release().unwrap_or(false) + }; + match result { + Ok(mut response) => { + if !cleanup_succeeded { + response.review_cleanup_warning = Some("review_cache_cleanup_failed_after_fixture_enrollment"); + } + Ok(response) + } + Err(_) if !cleanup_succeeded => Err(tally_command_error( + "fixture_enrollment_retry_state_uncertain", "Operation", + "The local fixture enrollment did not complete cleanly and the reviewed cache could not be released.", + "after_change", true, "Restart Bridge, probe again, and inspect local fixture status before retrying.", + )), + Err(error) => Err(error), + } +} + +#[tauri::command] +pub async fn tally_write_fixture_enrollment_status( + request: TallyWriteFixtureCompanyRequest, + mirror: State<'_, TallyMirrorRepository>, +) -> Result { + mirror + .write_fixture_enrollment_status(&request.mirror_company_id) + .await + .map_err(|_| { + tally_command_error( + "fixture_enrollment_status_unavailable", + "Operation", + "The local fixture status is unavailable.", + "safe", + false, + "Save a reviewed company scope before checking fixture status.", + ) + }) +} + +#[tauri::command] +pub async fn revoke_tally_write_fixture_enrollment( + request: TallyWriteFixtureCompanyRequest, + mirror: State<'_, TallyMirrorRepository>, +) -> Result { + mirror + .revoke_write_fixture_enrollment( + &request.mirror_company_id, + chrono::Utc::now().timestamp_millis(), + ) + .await + .map_err(|_| { + tally_command_error( + "fixture_enrollment_revoke_failed", + "Operation", + "The local fixture enrollment could not be revoked.", + "safe", + false, + "Check the saved company scope and retry; no Tally request was made.", + ) + }) +} + #[derive(Debug, Serialize)] pub struct PersistedTallyCompany { pub name: String, diff --git a/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql b/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql new file mode 100644 index 0000000..dacf3c1 --- /dev/null +++ b/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql @@ -0,0 +1,85 @@ +CREATE TABLE IF NOT EXISTS tally_write_fixture_enrollments ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + review_commitment_sha256 TEXT NOT NULL UNIQUE CHECK ( + length(review_commitment_sha256) = 64 AND + review_commitment_sha256 NOT GLOB '*[^0-9a-f]*' + ), + enrollment_payload_sha256 TEXT NOT NULL CHECK ( + length(enrollment_payload_sha256) = 64 AND + enrollment_payload_sha256 NOT GLOB '*[^0-9a-f]*' + ), + contract_version INTEGER NOT NULL CHECK (contract_version = 1), + disposable_company_attested INTEGER NOT NULL CHECK (disposable_company_attested = 1), + no_customer_data_attested INTEGER NOT NULL CHECK (no_customer_data_attested = 1), + backup_guidance_acknowledged INTEGER NOT NULL CHECK (backup_guidance_acknowledged = 1), + enrolled_at_unix_ms INTEGER NOT NULL CHECK (enrolled_at_unix_ms > 0), + FOREIGN KEY (company_id) REFERENCES tally_companies(id) ON DELETE RESTRICT +); + +CREATE TABLE IF NOT EXISTS tally_write_fixture_revocations ( + id TEXT PRIMARY KEY, + enrollment_id TEXT NOT NULL UNIQUE, + revocation_payload_sha256 TEXT NOT NULL CHECK ( + length(revocation_payload_sha256) = 64 AND + revocation_payload_sha256 NOT GLOB '*[^0-9a-f]*' + ), + safe_reason_code TEXT NOT NULL CHECK (safe_reason_code = 'operator_revoked'), + revoked_at_unix_ms INTEGER NOT NULL CHECK (revoked_at_unix_ms > 0), + FOREIGN KEY (enrollment_id) REFERENCES tally_write_fixture_enrollments(id) ON DELETE RESTRICT +); + +CREATE INDEX IF NOT EXISTS idx_tally_write_fixture_enrollments_company + ON tally_write_fixture_enrollments(company_id, enrolled_at_unix_ms); + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_enrollment_requires_observed_company +BEFORE INSERT ON tally_write_fixture_enrollments +WHEN NOT EXISTS ( + SELECT 1 FROM tally_companies + WHERE id = NEW.company_id AND identity_confidence = 'observed' + AND company_guid IS NOT NULL AND TRIM(company_guid) <> '' +) +BEGIN + SELECT RAISE(ABORT, 'fixture enrollment requires observed company identity'); +END; + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_enrollment_one_active_per_company +BEFORE INSERT ON tally_write_fixture_enrollments +WHEN EXISTS ( + SELECT 1 FROM tally_write_fixture_enrollments AS existing + WHERE existing.company_id = NEW.company_id + AND NOT EXISTS ( + SELECT 1 FROM tally_write_fixture_revocations AS revocation + WHERE revocation.enrollment_id = existing.id + ) +) +BEGIN + SELECT RAISE(ABORT, 'active fixture enrollment already exists'); +END; + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_enrollments_no_update +BEFORE UPDATE ON tally_write_fixture_enrollments +BEGIN + SELECT RAISE(ABORT, 'fixture enrollments are immutable'); +END; + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_enrollments_no_delete +BEFORE DELETE ON tally_write_fixture_enrollments +BEGIN + SELECT RAISE(ABORT, 'fixture enrollments cannot be deleted'); +END; + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_revocations_no_update +BEFORE UPDATE ON tally_write_fixture_revocations +BEGIN + SELECT RAISE(ABORT, 'fixture revocations are immutable'); +END; + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_revocations_no_delete +BEFORE DELETE ON tally_write_fixture_revocations +BEGIN + SELECT RAISE(ABORT, 'fixture revocations cannot be deleted'); +END; + +INSERT OR IGNORE INTO tally_schema_migrations(version, description, applied_at_unix_ms) +VALUES (13, 'local Tally synthetic write-fixture enrollment and revocation evidence', 0); diff --git a/src-tauri/src/db/tally_mirror.rs b/src-tauri/src/db/tally_mirror.rs index 557848c..0121096 100644 --- a/src-tauri/src/db/tally_mirror.rs +++ b/src-tauri/src/db/tally_mirror.rs @@ -26,6 +26,8 @@ const MIRROR_MIGRATION_V11: &str = include_str!("migrations/0011_tally_proof_record_counts_digest.sql"); const MIRROR_MIGRATION_V12: &str = include_str!("migrations/0012_tally_window_terminal_evidence.sql"); +const MIRROR_MIGRATION_V13: &str = + include_str!("migrations/0013_tally_write_fixture_enrollment.sql"); const MAX_WINDOW_STAGE_CHUNK: usize = 256; const MAX_WINDOW_EVIDENCE_JSON_BYTES: usize = 16 * 1024; @@ -335,6 +337,31 @@ pub struct PersistedCompanyProfilePage { pub truncated: bool, } +#[derive(Debug, Clone)] +pub struct WriteFixtureEnrollmentInput { + pub company_id: String, + pub review_commitment_sha256: String, + pub disposable_company_attested: bool, + pub no_customer_data_attested: bool, + pub backup_guidance_acknowledged: bool, + pub enrolled_at_unix_ms: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct WriteFixtureEnrollmentStatus { + pub fixture_state: &'static str, + pub enrolled_at_unix_ms: Option, + pub revoked_at_unix_ms: Option, + pub candidate_gate: &'static str, + pub write_capability: &'static str, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WriteFixtureEnrollmentRef { + pub id: String, + pub enrolled_at_unix_ms: i64, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct MirrorExplorerRecord { pub local_alias: String, @@ -885,6 +912,172 @@ impl TallyMirrorRepository { }) } + pub async fn enroll_write_fixture( + &self, + input: WriteFixtureEnrollmentInput, + ) -> Result { + validate_nonempty(&input.company_id, 128, "fixture_company_id")?; + validate_sha256(&input.review_commitment_sha256)?; + if !input.disposable_company_attested + || !input.no_customer_data_attested + || !input.backup_guidance_acknowledged + || input.enrolled_at_unix_ms <= 0 + { + return Err(MirrorError::InvalidInput("fixture_attestation")); + } + let pin = self.snapshot_source_pin(&input.company_id).await?; + let payload_sha256 = fixture_enrollment_payload_sha256(&FixtureEnrollmentCommitment { + schema: "bridge.tally.write-fixture-enrollment/1", + review_commitment_sha256: &input.review_commitment_sha256, + company_id: &pin.company_id, + canonical_origin: &pin.canonical_origin, + company_guid_ascii_casefolded: &pin.company_guid.to_ascii_lowercase(), + contract_version: 1, + disposable_company_attested: true, + no_customer_data_attested: true, + backup_guidance_acknowledged: true, + })?; + + let mut transaction = self.pool.begin().await?; + if let Some(row) = sqlx::query( + "SELECT id, enrollment_payload_sha256, enrolled_at_unix_ms \ + FROM tally_write_fixture_enrollments WHERE review_commitment_sha256 = ?1", + ) + .bind(&input.review_commitment_sha256) + .fetch_optional(&mut *transaction) + .await? + { + let existing_payload: String = row.try_get("enrollment_payload_sha256")?; + if existing_payload != payload_sha256 { + return Err(MirrorError::InvalidInput( + "fixture_review_commitment_reused", + )); + } + let result = WriteFixtureEnrollmentRef { + id: row.try_get("id")?, + enrolled_at_unix_ms: row.try_get("enrolled_at_unix_ms")?, + }; + transaction.commit().await?; + return Ok(result); + } + let id = Uuid::new_v4().to_string(); + sqlx::query( + "INSERT INTO tally_write_fixture_enrollments(\ + id, company_id, review_commitment_sha256, enrollment_payload_sha256, \ + contract_version, disposable_company_attested, no_customer_data_attested, \ + backup_guidance_acknowledged, enrolled_at_unix_ms\ + ) VALUES (?1, ?2, ?3, ?4, 1, 1, 1, 1, ?5)", + ) + .bind(&id) + .bind(&pin.company_id) + .bind(&input.review_commitment_sha256) + .bind(payload_sha256) + .bind(input.enrolled_at_unix_ms) + .execute(&mut *transaction) + .await?; + transaction.commit().await?; + Ok(WriteFixtureEnrollmentRef { + id, + enrolled_at_unix_ms: input.enrolled_at_unix_ms, + }) + } + + pub async fn write_fixture_enrollment_status( + &self, + company_id: &str, + ) -> Result { + validate_nonempty(company_id, 128, "fixture_company_id")?; + let row = sqlx::query( + "SELECT enrollment.enrolled_at_unix_ms, revocation.revoked_at_unix_ms \ + FROM tally_write_fixture_enrollments AS enrollment \ + LEFT JOIN tally_write_fixture_revocations AS revocation \ + ON revocation.enrollment_id = enrollment.id \ + WHERE enrollment.company_id = ?1 \ + ORDER BY (revocation.enrollment_id IS NULL) DESC, \ + enrollment.enrolled_at_unix_ms DESC, enrollment.id DESC LIMIT 1", + ) + .bind(company_id) + .fetch_optional(&self.pool) + .await?; + match row { + None => Ok(WriteFixtureEnrollmentStatus { + fixture_state: "not_enrolled", + enrolled_at_unix_ms: None, + revoked_at_unix_ms: None, + candidate_gate: "not_enrolled", + write_capability: "unknown", + }), + Some(row) => { + let revoked_at_unix_ms: Option = row.try_get("revoked_at_unix_ms")?; + Ok(WriteFixtureEnrollmentStatus { + fixture_state: if revoked_at_unix_ms.is_some() { + "revoked" + } else { + "active" + }, + enrolled_at_unix_ms: Some(row.try_get("enrolled_at_unix_ms")?), + revoked_at_unix_ms, + candidate_gate: if revoked_at_unix_ms.is_some() { + "not_enrolled" + } else { + "enrolled" + }, + write_capability: "unknown", + }) + } + } + } + + pub async fn revoke_write_fixture_enrollment( + &self, + company_id: &str, + revoked_at_unix_ms: i64, + ) -> Result { + validate_nonempty(company_id, 128, "fixture_company_id")?; + if revoked_at_unix_ms <= 0 { + return Err(MirrorError::InvalidInput("fixture_revoked_at")); + } + let mut transaction = self.pool.begin().await?; + let row = sqlx::query( + "SELECT enrollment.id, enrollment.enrollment_payload_sha256, revocation.revoked_at_unix_ms \ + FROM tally_write_fixture_enrollments AS enrollment \ + LEFT JOIN tally_write_fixture_revocations AS revocation \ + ON revocation.enrollment_id = enrollment.id \ + WHERE enrollment.company_id = ?1 \ + ORDER BY (revocation.enrollment_id IS NULL) DESC, \ + enrollment.enrolled_at_unix_ms DESC, enrollment.id DESC LIMIT 1", + ) + .bind(company_id) + .fetch_optional(&mut *transaction) + .await? + .ok_or(MirrorError::NotFound)?; + if row + .try_get::, _>("revoked_at_unix_ms")? + .is_none() + { + let enrollment_id: String = row.try_get("id")?; + let enrollment_payload_sha256: String = row.try_get("enrollment_payload_sha256")?; + let revocation_payload_sha256 = fixture_revocation_payload_sha256( + &enrollment_id, + &enrollment_payload_sha256, + revoked_at_unix_ms, + )?; + sqlx::query( + "INSERT INTO tally_write_fixture_revocations(\ + id, enrollment_id, revocation_payload_sha256, safe_reason_code, revoked_at_unix_ms\ + ) VALUES (?1, ?2, ?3, 'operator_revoked', ?4)", + ) + .bind(Uuid::new_v4().to_string()) + .bind(enrollment_id) + .bind(revocation_payload_sha256) + .bind(revoked_at_unix_ms) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + self.write_fixture_enrollment_status(company_id).await + } + /// Validates the encrypted capability receipt used by Core Accounting restart recovery. /// /// This is deliberately Core-specific. Other packs keep their own `Supported + Observed` @@ -1103,9 +1296,19 @@ impl TallyMirrorRepository { .execute(&mut *transaction) .await?; } + let write_fixture_enrollment_installed = sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM tally_schema_migrations WHERE version = 13", + ) + .fetch_one(&mut *transaction) + .await?; + if write_fixture_enrollment_installed == 0 { + sqlx::raw_sql(MIRROR_MIGRATION_V13) + .execute(&mut *transaction) + .await?; + } sqlx::query( "UPDATE tally_schema_migrations SET applied_at_unix_ms = ?1 \ - WHERE version IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) AND applied_at_unix_ms = 0", + WHERE version IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13) AND applied_at_unix_ms = 0", ) .bind(Utc::now().timestamp_millis()) .execute(&mut *transaction) @@ -4039,6 +4242,48 @@ fn sha256_json(value: &impl Serialize) -> Result { Ok(digest.iter().map(|byte| format!("{byte:02x}")).collect()) } +#[derive(Serialize)] +struct FixtureEnrollmentCommitment<'a> { + schema: &'static str, + review_commitment_sha256: &'a str, + company_id: &'a str, + canonical_origin: &'a str, + company_guid_ascii_casefolded: &'a str, + contract_version: u16, + disposable_company_attested: bool, + no_customer_data_attested: bool, + backup_guidance_acknowledged: bool, +} + +fn fixture_enrollment_payload_sha256( + material: &FixtureEnrollmentCommitment<'_>, +) -> Result { + sha256_json(material) +} + +#[derive(Serialize)] +struct FixtureRevocationCommitment<'a> { + schema: &'static str, + enrollment_id: &'a str, + enrollment_payload_sha256: &'a str, + safe_reason_code: &'static str, + revoked_at_unix_ms: i64, +} + +fn fixture_revocation_payload_sha256( + enrollment_id: &str, + enrollment_payload_sha256: &str, + revoked_at_unix_ms: i64, +) -> Result { + sha256_json(&FixtureRevocationCommitment { + schema: "bridge.tally.write-fixture-revocation/1", + enrollment_id, + enrollment_payload_sha256, + safe_reason_code: "operator_revoked", + revoked_at_unix_ms, + }) +} + fn hex_digest(bytes: impl AsRef<[u8]>) -> String { bytes .as_ref() @@ -4340,6 +4585,110 @@ mod tests { .is_err()); } + #[tokio::test] + async fn write_fixture_enrollment_is_idempotent_revocable_and_identity_safe() { + let repository = repository().await; + let saved = repository + .save_reviewed_setup(reviewed_setup_input(HASH_A)) + .await + .expect("persist observed company pin before local fixture enrollment"); + let input = WriteFixtureEnrollmentInput { + company_id: saved.company.id.clone(), + review_commitment_sha256: HASH_B.to_string(), + disposable_company_attested: true, + no_customer_data_attested: true, + backup_guidance_acknowledged: true, + enrolled_at_unix_ms: 3_000, + }; + + let first = repository + .enroll_write_fixture(input.clone()) + .await + .expect("locally enroll synthetic fixture"); + let replay = repository + .enroll_write_fixture(input.clone()) + .await + .expect("exact fixture enrollment replay is idempotent"); + assert_eq!(replay, first); + + let active = repository + .write_fixture_enrollment_status(&saved.company.id) + .await + .expect("read safe local fixture status"); + assert_eq!(active.fixture_state, "active"); + assert_eq!(active.candidate_gate, "enrolled"); + assert_eq!(active.write_capability, "unknown"); + let serialized = serde_json::to_string(&active).expect("serialize safe status"); + assert!(!serialized.contains("Synthetic Reviewed Company")); + assert!(!serialized.contains("reviewed-company-guid")); + + let mut competing = input; + competing.review_commitment_sha256 = HASH_A.to_string(); + competing.enrolled_at_unix_ms = 4_000; + assert!(repository.enroll_write_fixture(competing).await.is_err()); + + let revoked = repository + .revoke_write_fixture_enrollment(&saved.company.id, 5_000) + .await + .expect("append local revocation"); + assert_eq!(revoked.fixture_state, "revoked"); + assert_eq!(revoked.candidate_gate, "not_enrolled"); + assert_eq!( + repository + .revoke_write_fixture_enrollment(&saved.company.id, 6_000) + .await + .expect("repeat revocation is local and idempotent"), + revoked + ); + + let renewed = repository + .enroll_write_fixture(WriteFixtureEnrollmentInput { + company_id: saved.company.id.clone(), + review_commitment_sha256: HASH_A.to_string(), + disposable_company_attested: true, + no_customer_data_attested: true, + backup_guidance_acknowledged: true, + // Deliberately older than the revoked enrollment: wall clocks can roll back. + enrolled_at_unix_ms: 2_000, + }) + .await + .expect("a freshly reviewed fixture may enroll after revocation"); + assert_ne!(renewed.id, first.id); + assert_eq!( + repository + .write_fixture_enrollment_status(&saved.company.id) + .await + .expect("active enrollment wins over historical timestamp ordering") + .fixture_state, + "active" + ); + assert_eq!( + repository + .revoke_write_fixture_enrollment(&saved.company.id, 7_000) + .await + .expect("revoke the active enrollment despite clock rollback") + .fixture_state, + "revoked" + ); + assert_eq!( + sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM tally_write_fixture_revocations") + .fetch_one(&repository.pool) + .await + .expect("count immutable revocations"), + 2 + ); + assert!( + sqlx::query("UPDATE tally_write_fixture_enrollments SET enrolled_at_unix_ms = 1") + .execute(&repository.pool) + .await + .is_err() + ); + assert!(sqlx::query("DELETE FROM tally_write_fixture_revocations") + .execute(&repository.pool) + .await + .is_err()); + } + #[tokio::test] async fn reviewed_setup_atomically_persists_scoped_selected_read_evidence() { let repository = repository().await; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0f13573..8343119 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,6 +46,9 @@ pub fn run() { commands::bootstrap_direct_tally_company, commands::qualify_selected_tally_reads, commands::save_tally_setup, + commands::enroll_tally_write_fixture, + commands::tally_write_fixture_enrollment_status, + commands::revoke_tally_write_fixture_enrollment, commands::tally_persisted_company_profiles, commands::tally_mirror_explorer_page, commands::tally_sync_evidence, diff --git a/src/main.tsx b/src/main.tsx index 4069c21..6b1e61d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -110,6 +110,20 @@ type SavedTallySetup = { review_cleanup_warning?: "review_cache_cleanup_failed_after_save"; }; +type TallyWriteFixtureEnrollmentStatus = { + fixture_state: "not_enrolled" | "active" | "revoked"; + enrolled_at_unix_ms?: number; + revoked_at_unix_ms?: number; + candidate_gate: "not_enrolled" | "enrolled"; + write_capability: "unknown"; +}; + +type TallyWriteFixtureEnrollmentResponse = TallyWriteFixtureEnrollmentStatus & { + tally_requests_attempted: number; + tally_writes_attempted: number; + review_cleanup_warning?: "review_cache_cleanup_failed_after_fixture_enrollment"; +}; + type TallyProofSummary = { integrity_state: "entry_hash_valid"; run_id: string; @@ -352,7 +366,7 @@ type SelectedDocumentPath = { }; type View = "dashboard" | "companies" | "gst" | "mirror" | "dsc" | "documents" | "axal"; -type TallyAction = "probe" | "discover" | "bootstrap" | "qualify" | "save" | "ledgers" | "catalog" | "vouchers" | "evidence" | "explorer" | "start" | "resume" | "cancel"; +type TallyAction = "probe" | "discover" | "bootstrap" | "qualify" | "save" | "fixture_enroll" | "fixture_revoke" | "ledgers" | "catalog" | "vouchers" | "evidence" | "explorer" | "start" | "resume" | "cancel"; const TABLE_PREVIEW_LIMIT = 100; const MIRROR_PAGE_LIMIT = 25; @@ -666,6 +680,11 @@ function App() { const [qualificationFrom, setQualificationFrom] = React.useState(currentQualificationWindow.from); const [qualificationTo, setQualificationTo] = React.useState(currentQualificationWindow.to); const [companyError, setCompanyError] = React.useState(null); + const [fixtureStatus, setFixtureStatus] = React.useState(null); + const [fixtureStatusError, setFixtureStatusError] = React.useState(null); + const [fixtureDisposableAttested, setFixtureDisposableAttested] = React.useState(false); + const [fixtureNoCustomerDataAttested, setFixtureNoCustomerDataAttested] = React.useState(false); + const [fixtureBackupGuidanceAcknowledged, setFixtureBackupGuidanceAcknowledged] = React.useState(false); const [syncEvidence, setSyncEvidence] = React.useState(null); const [syncEvidenceError, setSyncEvidenceError] = React.useState(null); const [proofPreview, setProofPreview] = React.useState(null); @@ -850,6 +869,11 @@ function App() { clearSensitiveDiagnostics(); setDraft(null); setCompanyError(null); + setFixtureStatus(null); + setFixtureStatusError(null); + setFixtureDisposableAttested(false); + setFixtureNoCustomerDataAttested(false); + setFixtureBackupGuidanceAcknowledged(false); setSyncEvidence(null); setSyncEvidenceError(null); setProofPreview(null); @@ -870,6 +894,11 @@ function App() { } function clearSelectedCompanyScope() { + setFixtureStatus(null); + setFixtureStatusError(null); + setFixtureDisposableAttested(false); + setFixtureNoCustomerDataAttested(false); + setFixtureBackupGuidanceAcknowledged(false); clearCompanyScopedState({ clearQualifiedReadReview: () => { if (selectedReadScope) { @@ -1139,6 +1168,91 @@ function App() { } } + async function enrollWriteFixture() { + const company = companies.find((candidate) => tallyCompanyKey(candidate) === selectedCompany); + if (!reviewId || !reviewCommitmentSha256 || !company?.mirror_company_id || !company.guid || !selectedCompanyLive) { + setCompanyError("Probe again, select the persisted GUID-bearing company, and review it before locally enrolling a synthetic fixture."); + return; + } + if (!fixtureDisposableAttested || !fixtureNoCustomerDataAttested || !fixtureBackupGuidanceAcknowledged) { + setCompanyError("Confirm all three safeguards before enrolling the synthetic fixture."); + return; + } + const resultsVersion = tallyResultsVersion.current; + const reviewedCompanyKey = tallyCompanyKey(company); + const expectedReviewId = reviewId; + setTallyAction("fixture_enroll"); + setCompanyError(null); + try { + const result = await invoke("enroll_tally_write_fixture", { + request: { + config, + expected_review_id: reviewId, + expected_review_commitment_sha256: reviewCommitmentSha256, + mirror_company_id: company.mirror_company_id, + selected_company_guid: company.guid, + disposable_company_attested: fixtureDisposableAttested, + no_customer_data_attested: fixtureNoCustomerDataAttested, + backup_guidance_acknowledged: fixtureBackupGuidanceAcknowledged, + }, + }); + if (resultsVersion !== tallyResultsVersion.current || reviewedCompanyKey !== selectedCompany || expectedReviewId !== reviewId) return; + setFixtureStatus(result); + setFixtureDisposableAttested(false); + setFixtureNoCustomerDataAttested(false); + setFixtureBackupGuidanceAcknowledged(false); + setReviewId(null); + setReviewCommitmentSha256(null); + if (result.review_cleanup_warning) { + setCompanyError("The local fixture enrollment was saved, but its one-time in-memory review token could not be cleaned up. Restart Bridge before probing or enrolling another fixture."); + } + } catch (error) { + if (resultsVersion === tallyResultsVersion.current) setCompanyError(toOperatorError(error)); + } finally { + setTallyAction((current) => current === "fixture_enroll" ? null : current); + } + } + + async function revokeWriteFixture() { + const company = companies.find((candidate) => tallyCompanyKey(candidate) === selectedCompany); + if (!company?.mirror_company_id) { + setCompanyError("Select a persisted company before revoking its local fixture enrollment."); + return; + } + const resultsVersion = tallyResultsVersion.current; + const companyKey = tallyCompanyKey(company); + setTallyAction("fixture_revoke"); + setCompanyError(null); + try { + const status = await invoke("revoke_tally_write_fixture_enrollment", { + request: { mirror_company_id: company.mirror_company_id }, + }); + if (resultsVersion !== tallyResultsVersion.current || companyKey !== selectedCompany) return; + setFixtureStatus(status); + } catch (error) { + if (resultsVersion === tallyResultsVersion.current) setCompanyError(toOperatorError(error)); + } finally { + setTallyAction((current) => current === "fixture_revoke" ? null : current); + } + } + + async function refreshWriteFixtureStatus(mirrorCompanyId: string) { + setFixtureStatus(null); + setFixtureStatusError(null); + try { + const status = await invoke("tally_write_fixture_enrollment_status", { + request: { mirror_company_id: mirrorCompanyId }, + }); + const current = companies.find((candidate) => tallyCompanyKey(candidate) === selectedCompany); + if (current?.mirror_company_id === mirrorCompanyId) setFixtureStatus(status); + } catch { + const current = companies.find((candidate) => tallyCompanyKey(candidate) === selectedCompany); + if (current?.mirror_company_id === mirrorCompanyId) { + setFixtureStatusError("Bridge could not read the local fixture state. Retry before changing this local gate."); + } + } + } + async function cancelTallyRequest(requestId: string) { try { const cancelled = await invoke("cancel_tally_request", { requestId }); @@ -1731,6 +1845,25 @@ function App() { const gstDraftComplete = draft !== null && draft.missing_fields.length === 0; const selectedCompanyRecord = companies.find((company) => tallyCompanyKey(company) === selectedCompany); const selectedCompanyLive = !!selectedCompanyRecord && liveCompanyKeys.includes(tallyCompanyKey(selectedCompanyRecord)); + React.useEffect(() => { + const mirrorCompanyId = selectedCompanyRecord?.mirror_company_id; + setFixtureStatus(null); + setFixtureStatusError(null); + if (!mirrorCompanyId) return; + let cancelled = false; + void invoke("tally_write_fixture_enrollment_status", { + request: { mirror_company_id: mirrorCompanyId }, + }) + .then((status) => { + if (!cancelled) setFixtureStatus(status); + }) + .catch(() => { + if (!cancelled) setFixtureStatusError("Bridge could not read the local fixture state. Retry before changing this local gate."); + }); + return () => { + cancelled = true; + }; + }, [selectedCompanyRecord?.mirror_company_id]); const selectedRecentSnapshotRuns = selectedCompanyRecord?.mirror_company_id ? recentSnapshotRuns.filter((run) => run.mirror_company_id === selectedCompanyRecord.mirror_company_id) : []; @@ -2181,6 +2314,78 @@ function App() { {passportSnapshotId ? "Reviewed scope saved" : "Save reviewed company scope"}

This explicit save atomically stores the current Passport, the selected company pin, and any exact selected-read scope evidence. Probing and qualification alone do not write local setup state or anything to Tally.

+
+

Synthetic write-canary fixture

+

This is a local, revocable enrollment gate for a future canary. It sends no Tally request, performs no Tally write, and leaves write capability Unknown.

+
+
Local fixture state
{fixtureStatusError ? "Unavailable" : fixtureStatus ? formatIdentifier(fixtureStatus.fixture_state) : "Checking local state"}
+
Candidate gate
{fixtureStatus ? formatIdentifier(fixtureStatus.candidate_gate) : "Not checked"}
+
Enrolled locally
{formatRuntimeTime(fixtureStatus?.enrolled_at_unix_ms)}
+
Revoked locally
{formatRuntimeTime(fixtureStatus?.revoked_at_unix_ms)}
+
Write capability
Unknown
+
+ {fixtureStatusError && ( +
+

{fixtureStatusError}

+ {selectedCompanyRecord?.mirror_company_id && ( + + )} +
+ )} + + +

Backup guidance: before any later canary, create an offline backup, record how to restore it, and verify the restore path on a separate copy. If that is not possible, leave the next acknowledgement unchecked and do not proceed.

+ +
+ + +
+

An existing demo company is not automatically eligible: operator attestation is a gate, not proof of disposability. Revocation only changes this local gate; it does not alter Tally.

+ {!reviewId || !reviewCommitmentSha256 ?

Next: run a fresh Probe and review the selected company before local enrollment. Saving a reviewed company scope consumes its earlier review.

: null} +
)} From 5e9e94c08bcfa2f328bf3df49c36eddbc0f7eda4 Mon Sep 17 00:00:00 2001 From: lamemustafa Date: Wed, 22 Jul 2026 08:32:23 +0530 Subject: [PATCH 2/5] fix(tally): preserve latest fixture revocation evidence --- .../compatibility/compatibility-matrix.json | 2 +- .../compatibility/compatibility-surface.json | 4 +- src-tauri/src/db/tally_mirror.rs | 52 +++++++++++++------ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index b853f50..f82e8cd 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "616fdb4806bd4e78cfd184ac58a6b2faab53e60a9ca943e411124a39be276ffc", + "compatibility_surface_sha256": "061849e21495671a6432babf45538f3733e9ad36347f953603e8e9621d297a6c", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 0547d16..4dcb5b5 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -283,7 +283,7 @@ }, { "path": "src-tauri/src/db/tally_mirror.rs", - "sha256": "f6aaa9d7bb1e3ba9a8aab32049a4d3210eec0e6f8034c5071ad7d33916be88fa" + "sha256": "475c3499b6dc8536dbd94ea49e87cbcb4a413da23992567580f01963dedf7e39" }, { "path": "src-tauri/src/db/tally_write_store.rs", @@ -354,5 +354,5 @@ "sha256": "5a5c6eaaba234c3cbda52dfa040ed3314f79e535f744b87bc14d8d76a2299811" } ], - "manifest_sha256": "616fdb4806bd4e78cfd184ac58a6b2faab53e60a9ca943e411124a39be276ffc" + "manifest_sha256": "061849e21495671a6432babf45538f3733e9ad36347f953603e8e9621d297a6c" } diff --git a/src-tauri/src/db/tally_mirror.rs b/src-tauri/src/db/tally_mirror.rs index 0121096..892f28c 100644 --- a/src-tauri/src/db/tally_mirror.rs +++ b/src-tauri/src/db/tally_mirror.rs @@ -994,6 +994,7 @@ impl TallyMirrorRepository { ON revocation.enrollment_id = enrollment.id \ WHERE enrollment.company_id = ?1 \ ORDER BY (revocation.enrollment_id IS NULL) DESC, \ + revocation.revoked_at_unix_ms DESC, \ enrollment.enrolled_at_unix_ms DESC, enrollment.id DESC LIMIT 1", ) .bind(company_id) @@ -1039,22 +1040,23 @@ impl TallyMirrorRepository { } let mut transaction = self.pool.begin().await?; let row = sqlx::query( - "SELECT enrollment.id, enrollment.enrollment_payload_sha256, revocation.revoked_at_unix_ms \ + "SELECT enrollment.id, enrollment.enrollment_payload_sha256, \ + enrollment.enrolled_at_unix_ms, revocation.revoked_at_unix_ms \ FROM tally_write_fixture_enrollments AS enrollment \ LEFT JOIN tally_write_fixture_revocations AS revocation \ ON revocation.enrollment_id = enrollment.id \ WHERE enrollment.company_id = ?1 \ ORDER BY (revocation.enrollment_id IS NULL) DESC, \ + revocation.revoked_at_unix_ms DESC, \ enrollment.enrolled_at_unix_ms DESC, enrollment.id DESC LIMIT 1", ) .bind(company_id) .fetch_optional(&mut *transaction) .await? .ok_or(MirrorError::NotFound)?; - if row - .try_get::, _>("revoked_at_unix_ms")? - .is_none() - { + let enrolled_at_unix_ms: i64 = row.try_get("enrolled_at_unix_ms")?; + let existing_revocation: Option = row.try_get("revoked_at_unix_ms")?; + let status = if existing_revocation.is_none() { let enrollment_id: String = row.try_get("id")?; let enrollment_payload_sha256: String = row.try_get("enrollment_payload_sha256")?; let revocation_payload_sha256 = fixture_revocation_payload_sha256( @@ -1073,9 +1075,24 @@ impl TallyMirrorRepository { .bind(revoked_at_unix_ms) .execute(&mut *transaction) .await?; - } + WriteFixtureEnrollmentStatus { + fixture_state: "revoked", + enrolled_at_unix_ms: Some(enrolled_at_unix_ms), + revoked_at_unix_ms: Some(revoked_at_unix_ms), + candidate_gate: "not_enrolled", + write_capability: "unknown", + } + } else { + WriteFixtureEnrollmentStatus { + fixture_state: "revoked", + enrolled_at_unix_ms: Some(enrolled_at_unix_ms), + revoked_at_unix_ms: existing_revocation, + candidate_gate: "not_enrolled", + write_capability: "unknown", + } + }; transaction.commit().await?; - self.write_fixture_enrollment_status(company_id).await + Ok(status) } /// Validates the encrypted capability receipt used by Core Accounting restart recovery. @@ -4633,6 +4650,7 @@ mod tests { .expect("append local revocation"); assert_eq!(revoked.fixture_state, "revoked"); assert_eq!(revoked.candidate_gate, "not_enrolled"); + assert_eq!(revoked.revoked_at_unix_ms, Some(5_000)); assert_eq!( repository .revoke_write_fixture_enrollment(&saved.company.id, 6_000) @@ -4662,14 +4680,18 @@ mod tests { .fixture_state, "active" ); - assert_eq!( - repository - .revoke_write_fixture_enrollment(&saved.company.id, 7_000) - .await - .expect("revoke the active enrollment despite clock rollback") - .fixture_state, - "revoked" - ); + let final_revocation = repository + .revoke_write_fixture_enrollment(&saved.company.id, 7_000) + .await + .expect("revoke the active enrollment despite clock rollback"); + assert_eq!(final_revocation.fixture_state, "revoked"); + assert_eq!(final_revocation.revoked_at_unix_ms, Some(7_000)); + let latest_revoked = repository + .write_fixture_enrollment_status(&saved.company.id) + .await + .expect("latest revoked evidence uses revocation ordering"); + assert_eq!(latest_revoked.fixture_state, "revoked"); + assert_eq!(latest_revoked.revoked_at_unix_ms, Some(7_000)); assert_eq!( sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM tally_write_fixture_revocations") .fetch_one(&repository.pool) From 54fcef798184ad5f42583a48657945e9d58bc2d1 Mon Sep 17 00:00:00 2001 From: lamemustafa Date: Wed, 22 Jul 2026 08:44:16 +0530 Subject: [PATCH 3/5] fix(tally): sequence fixture revocation evidence --- .../compatibility/compatibility-matrix.json | 2 +- .../compatibility/compatibility-surface.json | 6 +++--- .../0013_tally_write_fixture_enrollment.sql | 3 ++- src-tauri/src/db/tally_mirror.rs | 17 ++++++++++++----- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index f82e8cd..06031b7 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "061849e21495671a6432babf45538f3733e9ad36347f953603e8e9621d297a6c", + "compatibility_surface_sha256": "0ede64ed6ea69bb8dcf44e647d4a189c81a31e7d3d20b083b3aea0d0c05ef677", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 4dcb5b5..624d67e 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -275,7 +275,7 @@ }, { "path": "src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql", - "sha256": "8e63b553b85d59bb10b149eb2b3497504e4e443690228cb2975ce325d9e84168" + "sha256": "402aa279eaeacf8e9eaed2fa5fd8d4c5e4bb696f7c4f15aae6df7ce710c8819e" }, { "path": "src-tauri/src/db/tally_incremental.rs", @@ -283,7 +283,7 @@ }, { "path": "src-tauri/src/db/tally_mirror.rs", - "sha256": "475c3499b6dc8536dbd94ea49e87cbcb4a413da23992567580f01963dedf7e39" + "sha256": "b2a880bbf9e44712e6249e1df8174a79274df9eb67e0cf12e8bd1d3d838eda2c" }, { "path": "src-tauri/src/db/tally_write_store.rs", @@ -354,5 +354,5 @@ "sha256": "5a5c6eaaba234c3cbda52dfa040ed3314f79e535f744b87bc14d8d76a2299811" } ], - "manifest_sha256": "061849e21495671a6432babf45538f3733e9ad36347f953603e8e9621d297a6c" + "manifest_sha256": "0ede64ed6ea69bb8dcf44e647d4a189c81a31e7d3d20b083b3aea0d0c05ef677" } diff --git a/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql b/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql index dacf3c1..882078d 100644 --- a/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql +++ b/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql @@ -18,7 +18,8 @@ CREATE TABLE IF NOT EXISTS tally_write_fixture_enrollments ( ); CREATE TABLE IF NOT EXISTS tally_write_fixture_revocations ( - id TEXT PRIMARY KEY, + event_sequence INTEGER PRIMARY KEY AUTOINCREMENT, + id TEXT NOT NULL UNIQUE, enrollment_id TEXT NOT NULL UNIQUE, revocation_payload_sha256 TEXT NOT NULL CHECK ( length(revocation_payload_sha256) = 64 AND diff --git a/src-tauri/src/db/tally_mirror.rs b/src-tauri/src/db/tally_mirror.rs index 892f28c..5e00ff0 100644 --- a/src-tauri/src/db/tally_mirror.rs +++ b/src-tauri/src/db/tally_mirror.rs @@ -994,7 +994,7 @@ impl TallyMirrorRepository { ON revocation.enrollment_id = enrollment.id \ WHERE enrollment.company_id = ?1 \ ORDER BY (revocation.enrollment_id IS NULL) DESC, \ - revocation.revoked_at_unix_ms DESC, \ + revocation.event_sequence DESC, \ enrollment.enrolled_at_unix_ms DESC, enrollment.id DESC LIMIT 1", ) .bind(company_id) @@ -1047,7 +1047,7 @@ impl TallyMirrorRepository { ON revocation.enrollment_id = enrollment.id \ WHERE enrollment.company_id = ?1 \ ORDER BY (revocation.enrollment_id IS NULL) DESC, \ - revocation.revoked_at_unix_ms DESC, \ + revocation.event_sequence DESC, \ enrollment.enrolled_at_unix_ms DESC, enrollment.id DESC LIMIT 1", ) .bind(company_id) @@ -4681,17 +4681,24 @@ mod tests { "active" ); let final_revocation = repository - .revoke_write_fixture_enrollment(&saved.company.id, 7_000) + .revoke_write_fixture_enrollment(&saved.company.id, 1_000) .await .expect("revoke the active enrollment despite clock rollback"); assert_eq!(final_revocation.fixture_state, "revoked"); - assert_eq!(final_revocation.revoked_at_unix_ms, Some(7_000)); + assert_eq!(final_revocation.revoked_at_unix_ms, Some(1_000)); let latest_revoked = repository .write_fixture_enrollment_status(&saved.company.id) .await .expect("latest revoked evidence uses revocation ordering"); assert_eq!(latest_revoked.fixture_state, "revoked"); - assert_eq!(latest_revoked.revoked_at_unix_ms, Some(7_000)); + assert_eq!(latest_revoked.revoked_at_unix_ms, Some(1_000)); + assert_eq!( + repository + .revoke_write_fixture_enrollment(&saved.company.id, 500) + .await + .expect("repeat revocation reports the latest committed evidence"), + latest_revoked + ); assert_eq!( sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM tally_write_fixture_revocations") .fetch_one(&repository.pool) From 2a1856828be9fb5754a2801296e11cf7b476c03a Mon Sep 17 00:00:00 2001 From: lamemustafa Date: Wed, 22 Jul 2026 09:04:13 +0530 Subject: [PATCH 4/5] fix(tally): migrate fixture revocation sequence --- .../compatibility/compatibility-matrix.json | 2 +- .../compatibility/compatibility-surface.json | 10 +- .../0013_tally_write_fixture_enrollment.sql | 3 +- ...ally_write_fixture_revocation_sequence.sql | 27 +++++ src-tauri/src/db/tally_mirror.rs | 114 +++++++++++++++++- 5 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence.sql diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index 06031b7..f22192e 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "0ede64ed6ea69bb8dcf44e647d4a189c81a31e7d3d20b083b3aea0d0c05ef677", + "compatibility_surface_sha256": "c7ea4688defca043e9070f958e359582a9cd1f7ad946321fc8285052f14db2f3", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 624d67e..08fd5b8 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -275,7 +275,11 @@ }, { "path": "src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql", - "sha256": "402aa279eaeacf8e9eaed2fa5fd8d4c5e4bb696f7c4f15aae6df7ce710c8819e" + "sha256": "8e63b553b85d59bb10b149eb2b3497504e4e443690228cb2975ce325d9e84168" + }, + { + "path": "src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence.sql", + "sha256": "9b8b5e4148c24e102c395b4d052050f24f53099f5d7477abf10fb0a3a8d18daf" }, { "path": "src-tauri/src/db/tally_incremental.rs", @@ -283,7 +287,7 @@ }, { "path": "src-tauri/src/db/tally_mirror.rs", - "sha256": "b2a880bbf9e44712e6249e1df8174a79274df9eb67e0cf12e8bd1d3d838eda2c" + "sha256": "93c4c6bcc659f9e5c9d121b6cb5c241bb168351142bc15af08bddd94dbefef74" }, { "path": "src-tauri/src/db/tally_write_store.rs", @@ -354,5 +358,5 @@ "sha256": "5a5c6eaaba234c3cbda52dfa040ed3314f79e535f744b87bc14d8d76a2299811" } ], - "manifest_sha256": "0ede64ed6ea69bb8dcf44e647d4a189c81a31e7d3d20b083b3aea0d0c05ef677" + "manifest_sha256": "c7ea4688defca043e9070f958e359582a9cd1f7ad946321fc8285052f14db2f3" } diff --git a/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql b/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql index 882078d..dacf3c1 100644 --- a/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql +++ b/src-tauri/src/db/migrations/0013_tally_write_fixture_enrollment.sql @@ -18,8 +18,7 @@ CREATE TABLE IF NOT EXISTS tally_write_fixture_enrollments ( ); CREATE TABLE IF NOT EXISTS tally_write_fixture_revocations ( - event_sequence INTEGER PRIMARY KEY AUTOINCREMENT, - id TEXT NOT NULL UNIQUE, + id TEXT PRIMARY KEY, enrollment_id TEXT NOT NULL UNIQUE, revocation_payload_sha256 TEXT NOT NULL CHECK ( length(revocation_payload_sha256) = 64 AND diff --git a/src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence.sql b/src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence.sql new file mode 100644 index 0000000..07f1765 --- /dev/null +++ b/src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence.sql @@ -0,0 +1,27 @@ +DROP TRIGGER IF EXISTS tally_write_fixture_revocations_no_update; + +ALTER TABLE tally_write_fixture_revocations + ADD COLUMN event_sequence INTEGER NOT NULL DEFAULT 0; + +UPDATE tally_write_fixture_revocations +SET event_sequence = rowid +WHERE event_sequence = 0; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_tally_write_fixture_revocations_sequence + ON tally_write_fixture_revocations(event_sequence); + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_revocations_require_sequence +BEFORE INSERT ON tally_write_fixture_revocations +WHEN NEW.event_sequence <= 0 +BEGIN + SELECT RAISE(ABORT, 'fixture revocation requires durable sequence'); +END; + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_revocations_no_update +BEFORE UPDATE ON tally_write_fixture_revocations +BEGIN + SELECT RAISE(ABORT, 'fixture revocations are immutable'); +END; + +INSERT OR IGNORE INTO tally_schema_migrations(version, description, applied_at_unix_ms) +VALUES (14, 'durable sequence for local Tally synthetic write-fixture revocations', 0); diff --git a/src-tauri/src/db/tally_mirror.rs b/src-tauri/src/db/tally_mirror.rs index 5e00ff0..4dadb3e 100644 --- a/src-tauri/src/db/tally_mirror.rs +++ b/src-tauri/src/db/tally_mirror.rs @@ -28,6 +28,8 @@ const MIRROR_MIGRATION_V12: &str = include_str!("migrations/0012_tally_window_terminal_evidence.sql"); const MIRROR_MIGRATION_V13: &str = include_str!("migrations/0013_tally_write_fixture_enrollment.sql"); +const MIRROR_MIGRATION_V14: &str = + include_str!("migrations/0014_tally_write_fixture_revocation_sequence.sql"); const MAX_WINDOW_STAGE_CHUNK: usize = 256; const MAX_WINDOW_EVIDENCE_JSON_BYTES: usize = 16 * 1024; @@ -1066,8 +1068,9 @@ impl TallyMirrorRepository { )?; sqlx::query( "INSERT INTO tally_write_fixture_revocations(\ - id, enrollment_id, revocation_payload_sha256, safe_reason_code, revoked_at_unix_ms\ - ) VALUES (?1, ?2, ?3, 'operator_revoked', ?4)", + event_sequence, id, enrollment_id, revocation_payload_sha256, safe_reason_code, revoked_at_unix_ms\ + ) VALUES ((SELECT COALESCE(MAX(event_sequence), 0) + 1 FROM tally_write_fixture_revocations), \ + ?1, ?2, ?3, 'operator_revoked', ?4)", ) .bind(Uuid::new_v4().to_string()) .bind(enrollment_id) @@ -1323,9 +1326,19 @@ impl TallyMirrorRepository { .execute(&mut *transaction) .await?; } + let write_fixture_revocation_sequence_installed = sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM tally_schema_migrations WHERE version = 14", + ) + .fetch_one(&mut *transaction) + .await?; + if write_fixture_revocation_sequence_installed == 0 { + sqlx::raw_sql(MIRROR_MIGRATION_V14) + .execute(&mut *transaction) + .await?; + } sqlx::query( "UPDATE tally_schema_migrations SET applied_at_unix_ms = ?1 \ - WHERE version IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13) AND applied_at_unix_ms = 0", + WHERE version IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) AND applied_at_unix_ms = 0", ) .bind(Utc::now().timestamp_millis()) .execute(&mut *transaction) @@ -4373,6 +4386,44 @@ mod tests { TallyMirrorRepository::new(pool) } + async fn repository_through_v13() -> TallyMirrorRepository { + let pool = SqlitePoolOptions::new() + .max_connections(1) + .after_connect(|connection, _| { + Box::pin(async move { + sqlx::query("PRAGMA foreign_keys = ON") + .execute(connection) + .await?; + Ok(()) + }) + }) + .connect("sqlite::memory:") + .await + .expect("connect to v13 in-memory SQLite"); + let mut transaction = pool.begin().await.expect("begin v13 migration"); + for migration in [ + MIRROR_MIGRATION_V2, + MIRROR_MIGRATION_V3, + MIRROR_MIGRATION_V4, + MIRROR_MIGRATION_V5, + MIRROR_MIGRATION_V6, + MIRROR_MIGRATION_V7, + MIRROR_MIGRATION_V8, + MIRROR_MIGRATION_V9, + MIRROR_MIGRATION_V10, + MIRROR_MIGRATION_V11, + MIRROR_MIGRATION_V12, + MIRROR_MIGRATION_V13, + ] { + sqlx::raw_sql(migration) + .execute(&mut *transaction) + .await + .expect("apply migration through v13"); + } + transaction.commit().await.expect("commit v13 schema"); + TallyMirrorRepository::new(pool) + } + async fn seed_repository( repository: TallyMirrorRepository, ) -> (TallyMirrorRepository, CapabilitySnapshotRef, CompanyRef) { @@ -4718,6 +4769,63 @@ mod tests { .is_err()); } + #[tokio::test] + async fn v13_fixture_revocations_upgrade_to_durable_sequence() { + let repository = repository_through_v13().await; + let saved = repository + .save_reviewed_setup(reviewed_setup_input(HASH_A)) + .await + .expect("seed observed company for legacy fixture evidence"); + sqlx::query( + "INSERT INTO tally_write_fixture_enrollments(\ + id, company_id, review_commitment_sha256, enrollment_payload_sha256, \ + contract_version, disposable_company_attested, no_customer_data_attested, \ + backup_guidance_acknowledged, enrolled_at_unix_ms\ + ) VALUES ('legacy-enrollment', ?1, ?2, ?3, 1, 1, 1, 1, 3000)", + ) + .bind(&saved.company.id) + .bind(HASH_B) + .bind(HASH_A) + .execute(&repository.pool) + .await + .expect("seed legacy enrollment"); + sqlx::query( + "INSERT INTO tally_write_fixture_revocations(\ + id, enrollment_id, revocation_payload_sha256, safe_reason_code, revoked_at_unix_ms\ + ) VALUES ('legacy-revocation', 'legacy-enrollment', ?1, 'operator_revoked', 4000)", + ) + .bind(HASH_B) + .execute(&repository.pool) + .await + .expect("seed legacy revocation"); + + repository + .migrate() + .await + .expect("upgrade fixture revocation evidence from v13 to v14"); + let status = repository + .write_fixture_enrollment_status(&saved.company.id) + .await + .expect("read upgraded legacy fixture status"); + assert_eq!(status.fixture_state, "revoked"); + assert_eq!(status.revoked_at_unix_ms, Some(4_000)); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT event_sequence FROM tally_write_fixture_revocations WHERE id = 'legacy-revocation'", + ) + .fetch_one(&repository.pool) + .await + .expect("read durable backfilled sequence"), + 1 + ); + assert!(sqlx::query( + "UPDATE tally_write_fixture_revocations SET event_sequence = 2 WHERE id = 'legacy-revocation'", + ) + .execute(&repository.pool) + .await + .is_err()); + } + #[tokio::test] async fn reviewed_setup_atomically_persists_scoped_selected_read_evidence() { let repository = repository().await; From b4419aacdcac16ae819e46602d2ab9df1a7e650b Mon Sep 17 00:00:00 2001 From: lamemustafa Date: Wed, 22 Jul 2026 11:10:22 +0530 Subject: [PATCH 5/5] fix(tally): support sequenced fixture upgrades --- .../compatibility/compatibility-matrix.json | 2 +- .../compatibility/compatibility-surface.json | 12 ++-- ...e_fixture_revocation_sequence_existing.sql | 18 +++++ src-tauri/src/db/tally_mirror.rs | 65 ++++++++++++++++++- src-tauri/src/sync/snapshot.rs | 7 +- src-tauri/src/tally/connector.rs | 15 +++-- 6 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence_existing.sql diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index f22192e..b6c5b81 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "c7ea4688defca043e9070f958e359582a9cd1f7ad946321fc8285052f14db2f3", + "compatibility_surface_sha256": "763a59fbd791a0bfffb4c45d95d45c8236c8dc93038d10b54bc88156ed1f80d4", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 08fd5b8..5bc9301 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -281,13 +281,17 @@ "path": "src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence.sql", "sha256": "9b8b5e4148c24e102c395b4d052050f24f53099f5d7477abf10fb0a3a8d18daf" }, + { + "path": "src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence_existing.sql", + "sha256": "48f6a803038dd7ce7fb36f8509920c7b286e26e99b0ca0c270f73c4e37aee414" + }, { "path": "src-tauri/src/db/tally_incremental.rs", "sha256": "bac2c859de102cf1e558f669dee0445a9406e95ee9923b6f52bd498ee768e73b" }, { "path": "src-tauri/src/db/tally_mirror.rs", - "sha256": "93c4c6bcc659f9e5c9d121b6cb5c241bb168351142bc15af08bddd94dbefef74" + "sha256": "6645bddc38b1c7b53522e7b6782ed377b61dca2e90fea2e6f1aef3065a98ca72" }, { "path": "src-tauri/src/db/tally_write_store.rs", @@ -307,7 +311,7 @@ }, { "path": "src-tauri/src/sync/snapshot.rs", - "sha256": "b6a61819affc3d8c28f86f641ea2956993d704a2ee099f71ee4ea60c17d711fa" + "sha256": "e5acb891420e568678b9f20e9adabe2fa247deb4569c2648b19d4b50480f55f7" }, { "path": "src-tauri/src/tally/capability_packs.rs", @@ -319,7 +323,7 @@ }, { "path": "src-tauri/src/tally/connector.rs", - "sha256": "dfc94110256fb205c4160e1671e0bdfe2d426ff85db194e8ebdb4634b96ab798" + "sha256": "665f3c4c8549048718b55786dc5d6f93caf251e973abc95d9ab79a3f7937dcd0" }, { "path": "src-tauri/src/tally/mod.rs", @@ -358,5 +362,5 @@ "sha256": "5a5c6eaaba234c3cbda52dfa040ed3314f79e535f744b87bc14d8d76a2299811" } ], - "manifest_sha256": "c7ea4688defca043e9070f958e359582a9cd1f7ad946321fc8285052f14db2f3" + "manifest_sha256": "763a59fbd791a0bfffb4c45d95d45c8236c8dc93038d10b54bc88156ed1f80d4" } diff --git a/src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence_existing.sql b/src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence_existing.sql new file mode 100644 index 0000000..b23d96a --- /dev/null +++ b/src-tauri/src/db/migrations/0014_tally_write_fixture_revocation_sequence_existing.sql @@ -0,0 +1,18 @@ +CREATE UNIQUE INDEX IF NOT EXISTS idx_tally_write_fixture_revocations_sequence + ON tally_write_fixture_revocations(event_sequence); + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_revocations_require_sequence +BEFORE INSERT ON tally_write_fixture_revocations +WHEN NEW.event_sequence <= 0 +BEGIN + SELECT RAISE(ABORT, 'fixture revocation requires durable sequence'); +END; + +CREATE TRIGGER IF NOT EXISTS tally_write_fixture_revocations_no_update +BEFORE UPDATE ON tally_write_fixture_revocations +BEGIN + SELECT RAISE(ABORT, 'fixture revocations are immutable'); +END; + +INSERT OR IGNORE INTO tally_schema_migrations(version, description, applied_at_unix_ms) +VALUES (14, 'durable sequence for local Tally synthetic write-fixture revocations', 0); diff --git a/src-tauri/src/db/tally_mirror.rs b/src-tauri/src/db/tally_mirror.rs index 4dadb3e..fa6906e 100644 --- a/src-tauri/src/db/tally_mirror.rs +++ b/src-tauri/src/db/tally_mirror.rs @@ -30,6 +30,8 @@ const MIRROR_MIGRATION_V13: &str = include_str!("migrations/0013_tally_write_fixture_enrollment.sql"); const MIRROR_MIGRATION_V14: &str = include_str!("migrations/0014_tally_write_fixture_revocation_sequence.sql"); +const MIRROR_MIGRATION_V14_ALREADY_SEQUENCED: &str = + include_str!("migrations/0014_tally_write_fixture_revocation_sequence_existing.sql"); const MAX_WINDOW_STAGE_CHUNK: usize = 256; const MAX_WINDOW_EVIDENCE_JSON_BYTES: usize = 16 * 1024; @@ -1332,9 +1334,20 @@ impl TallyMirrorRepository { .fetch_one(&mut *transaction) .await?; if write_fixture_revocation_sequence_installed == 0 { - sqlx::raw_sql(MIRROR_MIGRATION_V14) - .execute(&mut *transaction) - .await?; + let event_sequence_exists = sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM pragma_table_info('tally_write_fixture_revocations') \ + WHERE name = 'event_sequence'", + ) + .fetch_one(&mut *transaction) + .await? + != 0; + sqlx::raw_sql(if event_sequence_exists { + MIRROR_MIGRATION_V14_ALREADY_SEQUENCED + } else { + MIRROR_MIGRATION_V14 + }) + .execute(&mut *transaction) + .await?; } sqlx::query( "UPDATE tally_schema_migrations SET applied_at_unix_ms = ?1 \ @@ -4826,6 +4839,52 @@ mod tests { .is_err()); } + #[tokio::test] + async fn already_sequenced_v13_fixture_revocations_upgrade_idempotently() { + let repository = repository_through_v13().await; + let saved = repository + .save_reviewed_setup(reviewed_setup_input(HASH_A)) + .await + .expect("seed observed company for already-sequenced v13 fixture evidence"); + // Emulate the pre-merge v13 schema that already carried this column. + sqlx::query( + "ALTER TABLE tally_write_fixture_revocations \ + ADD COLUMN event_sequence INTEGER NOT NULL DEFAULT 0", + ) + .execute(&repository.pool) + .await + .expect("add pre-existing legacy event sequence"); + + repository + .migrate() + .await + .expect("upgrade already-sequenced v13 schema without duplicate column"); + repository + .enroll_write_fixture(WriteFixtureEnrollmentInput { + company_id: saved.company.id.clone(), + review_commitment_sha256: HASH_B.to_string(), + disposable_company_attested: true, + no_customer_data_attested: true, + backup_guidance_acknowledged: true, + enrolled_at_unix_ms: 3_000, + }) + .await + .expect("enroll after already-sequenced upgrade"); + repository + .revoke_write_fixture_enrollment(&saved.company.id, 4_000) + .await + .expect("revoke after already-sequenced upgrade"); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT event_sequence FROM tally_write_fixture_revocations LIMIT 1", + ) + .fetch_one(&repository.pool) + .await + .expect("read durable sequence after alternate upgrade path"), + 1 + ); + } + #[tokio::test] async fn reviewed_setup_atomically_persists_scoped_selected_read_evidence() { let repository = repository().await; diff --git a/src-tauri/src/sync/snapshot.rs b/src-tauri/src/sync/snapshot.rs index 799171e..112c612 100644 --- a/src-tauri/src/sync/snapshot.rs +++ b/src-tauri/src/sync/snapshot.rs @@ -3651,7 +3651,9 @@ mod tests { CapabilityItemInput, CapabilityKind, CapabilitySnapshotInput, CompanyInput, Confidence, RunOutcome, SourceIdentityInput, VerificationState, }; - use crate::tally::{RuntimeTallyConnector, TallyConfig, TallyRuntime}; + use crate::tally::{ + connector::simulator_test_lock, RuntimeTallyConnector, TallyConfig, TallyRuntime, + }; use super::*; @@ -4444,6 +4446,7 @@ mod tests { #[tokio::test] async fn simulator_transport_limit_splits_only_the_voucher_window_before_child_dispatch() { const TEST_RESPONSE_LIMIT: usize = 4 * 1024; + let _simulator_guard = simulator_test_lock().lock().await; let (_, mirror, store, mut plan) = setup().await; plan.resume_key = "resume-transport-adaptive-split".to_string(); plan.run_id = "run-transport-adaptive-split".to_string(); @@ -4511,12 +4514,12 @@ mod tests { let result = FullSnapshotEngine::new(&mirror, &crash_store, &connector) .run(&plan, &AtomicCancellation::default()) .await; - let requests = simulator.finish().unwrap(); let error = result.expect_err("stop after the split graph is durably persisted"); assert!(matches!( error, SnapshotError::StateInvariant("injected_crash_after_split_commit") )); + let requests = simulator.finish().unwrap(); let persisted = store.load(&plan.resume_key).await.unwrap().unwrap(); assert_eq!(persisted.windows.len(), 3); assert!(persisted.windows[&root.id].split.is_some()); diff --git a/src-tauri/src/tally/connector.rs b/src-tauri/src/tally/connector.rs index 15e2e8e..81b7759 100644 --- a/src-tauri/src/tally/connector.rs +++ b/src-tauri/src/tally/connector.rs @@ -524,21 +524,24 @@ fn hex_lower(bytes: &[u8]) -> String { output } +#[cfg(test)] +pub(crate) fn simulator_test_lock() -> &'static tokio::sync::Mutex<()> { + use std::sync::OnceLock; + + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| tokio::sync::Mutex::new(())) +} + #[cfg(test)] mod tests { use super::*; - use std::{net::SocketAddr, sync::OnceLock}; + use std::net::SocketAddr; use tally_protocol_simulator::{Fixture, ScenarioPlan, SequenceSimulator}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, task::JoinHandle, }; - fn simulator_test_lock() -> &'static tokio::sync::Mutex<()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| tokio::sync::Mutex::new(())) - } - async fn spawn_method_routed_server( post_responses: Vec, ) -> (SocketAddr, JoinHandle>) {