Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tally/compatibility/compatibility-matrix.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schema_version": 1,
"bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e",
"compatibility_surface_sha256": "0837a04c50b2f9ece55777c99a704137b1f781c28040bd9bb558a96d52a30b99",
"compatibility_surface_sha256": "763a59fbd791a0bfffb4c45d95d45c8236c8dc93038d10b54bc88156ed1f80d4",
"claims": [
{
"claim_id": "erp9-6-6-3-windows-education-xml-one-company",
Expand Down
30 changes: 23 additions & 7 deletions docs/tally/compatibility/compatibility-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -239,7 +243,7 @@
},
{
"path": "src-tauri/src/commands.rs",
"sha256": "dc3992dfab507e095c56239ad480aa111c671c0b540f98981803bcb6ab065417"
"sha256": "123ba26cff15d62a2b2c7aef0cb07ecb1ed78a86ec4ed0b96cdc825e62d324bf"
},
{
"path": "src-tauri/src/db/encrypted.rs",
Expand Down Expand Up @@ -269,21 +273,33 @@
"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/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": "07bbfb8b2d46588ca27b1bf1da2cab1c1c75aab5ecffa3909bbea914f8a047d9"
"sha256": "6645bddc38b1c7b53522e7b6782ed377b61dca2e90fea2e6f1aef3065a98ca72"
},
{
"path": "src-tauri/src/db/tally_write_store.rs",
"sha256": "628a33d2cb481fcb73393b26be51a6b4239f1c9c1eddab8a858dd5111851b91d"
},
{
"path": "src-tauri/src/lib.rs",
"sha256": "07aa3cd8152c347be13d409c40780e3b9acd9f18cd0de68d76e86480a172abf2"
"sha256": "9942f21406efda26c683d0056eac96bef9e38bff0c30f4f1487dfb304c8108a2"
},
{
"path": "src-tauri/src/sync/coordinator.rs",
Expand All @@ -295,7 +311,7 @@
},
{
"path": "src-tauri/src/sync/snapshot.rs",
"sha256": "b6a61819affc3d8c28f86f641ea2956993d704a2ee099f71ee4ea60c17d711fa"
"sha256": "e5acb891420e568678b9f20e9adabe2fa247deb4569c2648b19d4b50480f55f7"
},
{
"path": "src-tauri/src/tally/capability_packs.rs",
Expand All @@ -307,7 +323,7 @@
},
{
"path": "src-tauri/src/tally/connector.rs",
"sha256": "dfc94110256fb205c4160e1671e0bdfe2d426ff85db194e8ebdb4634b96ab798"
"sha256": "665f3c4c8549048718b55786dc5d6f93caf251e973abc95d9ab79a3f7937dcd0"
},
{
"path": "src-tauri/src/tally/mod.rs",
Expand Down Expand Up @@ -335,7 +351,7 @@
},
{
"path": "src/main.tsx",
"sha256": "c33c4d47c78dcbcc79c0406a8b83b5c35fda9a979c0c58146cd4a66b8e56dfa7"
"sha256": "c463fdca1e73a3f4a30a1ddc7f039dc87ba9f6f4c87f8334c94f2ffe750a6223"
},
{
"path": "src/styles.css",
Expand All @@ -346,5 +362,5 @@
"sha256": "5a5c6eaaba234c3cbda52dfa040ed3314f79e535f744b87bc14d8d76a2299811"
}
],
"manifest_sha256": "0837a04c50b2f9ece55777c99a704137b1f781c28040bd9bb558a96d52a30b99"
"manifest_sha256": "763a59fbd791a0bfffb4c45d95d45c8236c8dc93038d10b54bc88156ed1f80d4"
}
29 changes: 29 additions & 0 deletions docs/tally/compatibility/synthetic-write-canary-fixture.md
Original file line number Diff line number Diff line change
@@ -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.
209 changes: 208 additions & 1 deletion src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<TallyWriteFixtureEnrollmentResponse, TallyCommandError> {
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<TallyWriteFixtureEnrollmentResponse, TallyCommandError> = 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<WriteFixtureEnrollmentStatus, TallyCommandError> {
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<WriteFixtureEnrollmentStatus, TallyCommandError> {
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.",
Comment thread
lamemustafa marked this conversation as resolved.
)
})
}

#[derive(Debug, Serialize)]
pub struct PersistedTallyCompany {
pub name: String,
Expand Down
Loading