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
15 changes: 9 additions & 6 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,11 @@ call, discovery request, trust adoption, share, rescan, or Syncthing
configuration/ignore change. The unreleased app source can set upload evidence
after an exact pinned helper attestation and, only after an accepted upload,
download evidence from a fresh local apply of the exact authorized helper
response in the same active operation; roundtrip evidence remains unset. A
complete download acceptance has run only against injected test event streams
plus byte-exact artifacts from isolated local Syncthing instances; no download
has been observed on a physical device.
response in the same active operation, and derives the causal roundtrip only
from that one upload-then-download chain. A complete download acceptance has
run only against injected test event streams plus byte-exact artifacts from
isolated local Syncthing instances; no download or roundtrip has been observed
on a physical device.

Before the supported installer creates the namespace, the app must send a valid
signed enablement and the local operator must choose an exact existing Syncthing
Expand Down Expand Up @@ -268,8 +269,10 @@ signature, binding, digest, nonce, payload, and TTL validation of that file. A
response existing before the baseline, arriving after an engine restart, or
failing any validation can never set it; an invalid file at the exact path ends
the operation as a conflict, and every terminal outcome after upload keeps the
upload field visible as a partial result. The roundtrip field remains false and
cannot be inferred from upload or download.
upload field visible as a partial result. The separate roundtrip field derives
only when this same operation's upload and download acceptances complete for
the exact validated chain; it is a scoped causal propagation claim, never
global sync health, future delivery, byte accounting, or a direct-peer claim.

The active operation, request/query bytes, random values, digests, poll state,
and evidence are not persisted in preferences, Keychain, logs, telemetry,
Expand Down
27 changes: 16 additions & 11 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ succeeded” flag:
| Local data progress observed | Background run, or one eligible server/folder check | A fresh, successful incoming file application (`ItemFinished`) |
| Upload observed | Exact app/helper/homeserver/folder/operation correlation | Only an explicit foreground check in unreleased M5 app source can accept the exact paired-helper attestation for its active request/query. |
| Download observed | Exact controlled response correlation | Only the same active operation in unreleased M6 app source can set it: after an accepted upload, the authorized helper response must pass a fresh post-authorization cursor/wall-clock/generation `ItemFinished` gate plus complete validation. A helper response or synchronized file alone cannot set it. |
| Full roundtrip confirmed | One matching upload-then-download correlation | Not implemented; it requires the later causal derivation from the same active chain's upload and download. |
| Full roundtrip confirmed | One matching upload-then-download correlation | Only the same active operation in unreleased M7 app source can derive it, from exactly its accepted upload then accepted download. It is a scoped propagation claim, never global sync health or future delivery. |

None automatically implies the next. Relay reachability is not a trigger;
trigger observation is not APNs delivery; push receipt is not background start;
Expand All @@ -57,8 +57,9 @@ local data progress. A successful incoming file application proves that this
iPhone applied a file change, but not that network bytes moved, which peer
supplied every block, or that the check caused the change. Upload and controlled
download are separate, explicitly initiated Decision 024 fields in unreleased
source; download can derive only after an accepted upload inside the same active
operation. Roundtrip is not implemented, so it cannot be derived.
source; download can derive only after an accepted upload inside the same
active operation, and the causal roundtrip derives only inside that same
operation from exactly those two acceptances.

Server snapshots contain only entitlement, provisioning, backend, and
per-homeserver Relay observation. The v1 push contains no homeserver/folder
Expand Down Expand Up @@ -88,8 +89,8 @@ background sync.
Ignore rules, missing paths, an event-buffer overflow, or a runtime folder error
can prevent an observation and therefore end conservatively as incomplete; they
never create a false success. It remains separate from the explicit controlled
operation below and cannot populate that operation's evidence. Causal roundtrip
requires its later Decision 024 milestone. Relay v1 is unchanged.
operation below and cannot populate that operation's evidence. Relay v1 is
unchanged.

#### Opt-in correlated-roundtrip helper runtime — upload and controlled download

Expand All @@ -99,10 +100,11 @@ immutable digest plus upgrade, downgrade, and forward-recovery path are
verified. The source tree now also contains the unreleased app-side explicit
capability, pairing, credential-lifecycle, and namespace-authorization control
plane plus the explicit M5 foreground upload operation and the M6 controlled
download leg. Product upload is implemented to the exact signed-attestation
boundary and controlled download to the exact fresh-apply response boundary;
both remain unreleased, and causal roundtrip is unset. VaultSync 2.0
remains NO-GO.
download leg and the M7 causal-roundtrip derivation. Product upload is
implemented to the exact signed-attestation boundary, controlled download to
the exact fresh-apply response boundary, and the causal roundtrip derives
solely from those two acceptances of one operation; all remain unreleased.
VaultSync 2.0 remains NO-GO.

One upload operation begins only after a user tap and a second localized
confirmation. The app rechecks the exact current capability, pairing and
Expand Down Expand Up @@ -131,8 +133,11 @@ binding, digest, nonce, payload, and TTL validation sets `download observed`.
A response predating the baseline or authorization, an engine restart, a
changed binding, or any validation failure cannot set it; an invalid file at
the exact path ends the operation as a conflict. Every terminal outcome after
upload keeps the upload field visible as a partial result. Roundtrip remains
immutable false in this milestone.
upload keeps the upload field visible as a partial result. When the same
operation's upload and download acceptances both complete, the causal
roundtrip derives in that acceptance from exactly this chain; it claims
scoped propagation for one operation, never global health, future delivery,
byte counts, or a direct peer.

The runtime is gated by an operator-authored read-only configuration plus a
separate writable state directory. If either is absent, existing helpers retain
Expand Down
65 changes: 65 additions & 0 deletions docs/m7-causal-roundtrip-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# M7 causal-roundtrip readiness

**Status:** Unreleased app source. The causal roundtrip derivation of
[Decision 024](decisions/024-canonical-correlated-roundtrip-contract-and-threat-model.md)
step 10 is implemented on top of the M5 upload and M6 controlled-download
legs. No new message type, endpoint, helper, bridge, Relay, or wire change
ships with this milestone. VaultSync 2.0 remains NO-GO until the release and
rollout gates complete.

## Derivation rule

`roundtrip confirmed` is set in exactly one place: the download acceptance of
the same active operation. That acceptance has already validated the request,
attestation, authorization, response signature, app/helper keys, epochs,
homeserver/folder bindings, operation ID, nonces, digests, payloads, and TTL
for one explicit tuple, so the roundtrip derives from exactly this
upload-then-download chain and from nothing else.

- No timestamp, HTTP status, Relay observation, APNs, scan/index/idle state,
capability reachability, cleanup result, or tombstone can set it.
- A stale, replayed, copied, tampered, or foreign-operation response ends the
operation without download or roundtrip evidence; a response artifact copied
from another operation fails chain validation and terminates as conflict.
- Cancellation, restart, generation change, timeout, and rate limits keep the
partial upload field visible and never derive a roundtrip.
- The claim is scoped causal propagation for one operation. It is never
global sync health, future-delivery evidence, byte accounting, or a
direct-peer claim.

## Compatibility and rollback

The helper wire surface stays byte-identical to helper 2.0.2; capability
negotiation, pairing, namespace, upload, and response behavior are unchanged.
Old or downgraded helpers yield capability unavailable without fallback. App
or helper rollback preserves credentials, namespace authorization, opaque
artifact copies, backups, versions, conflicts, history, tombstones, mappings,
and user data. Retained copies never regain validity and cannot derive a
late roundtrip.

## Verification

All Xcode results and derived data are outside the repository under `/tmp`.
The local gate for this milestone includes:

- the M5/M6 runtime suites re-run with the derivation: the exact fresh chain
ends `roundtrip confirmed` with all three evidence fields set, and every
stale, tampered, generation-changed, cancelled, restarted, rate-limited,
and cross-operation-replay scenario keeps the roundtrip field false;
- the cross-operation replay property: a valid response artifact stolen from
a completed operation and republished at a second operation's exact path
is rejected by chain validation and ends as conflict;
- cross-language golden vectors with per-byte tamper rejection (unchanged);
- both isolated two-instance Syncthing E2E tests (upload and response
transport with the real helper foundation), re-run at this head;
- the complete iOS plan, a Release-configuration simulator build,
design-token lint, string-key parity, and the sync-proof privacy lint.

The signed owner-device test was not executed — owner-approved
physical-device waiver (2026-07-15). Simulator and isolated local Syncthing
evidence substitute for it; no hardware keychain behavior, real APNs
delivery, real background waking, or TestFlight installation on hardware is
claimed, and simulator evidence is never described as real-device evidence.

Decision 024 remains the unchanged canonical contract. Cleanup remains
evidence-orthogonal and a later milestone.
17 changes: 14 additions & 3 deletions ios/VaultSync/Services/DiagnosticsPairingController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ final class DiagnosticsPairingController {
case checking
case uploadObserved
case downloadObserved
case roundtripConfirmed
case cancelled
case timedOut
case interrupted
Expand All @@ -41,7 +42,7 @@ final class DiagnosticsPairingController {
struct UploadEvidence: Equatable, Sendable {
var uploadObserved = false
var downloadObserved = false
let roundtripConfirmed = false
var roundtripConfirmed = false
}

struct UploadStatus: Equatable, Sendable {
Expand Down Expand Up @@ -801,9 +802,19 @@ final class DiagnosticsPairingController {
// not a pinned-channel protocol mismatch.
throw DiagnosticsProtocolError.conflict
}
// D024 step 10: this exact acceptance already validated the
// request, attestation, authorization, keys, epochs, bindings,
// and TTL for the one active operation, so the causal roundtrip
// derives from exactly this upload-then-download chain and from
// nothing else. It is a scoped propagation claim, never global
// sync health or future-delivery evidence.
uploadStatuses[recordID] = UploadStatus(
phase: .downloadObserved,
evidence: UploadEvidence(uploadObserved: true, downloadObserved: true),
phase: .roundtripConfirmed,
evidence: UploadEvidence(
uploadObserved: true,
downloadObserved: true,
roundtripConfirmed: true
),
completedPolls: completedUploadPolls,
completedResponsePolls: index + 1
)
Expand Down
11 changes: 8 additions & 3 deletions ios/VaultSync/Views/ControlledDiagnosticsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ struct ControlledDiagnosticsView: View {
startPendingUpload()
}
} message: {
Text(L10n.tr("VaultSync will create one signed request with 256 random bytes in the already authorized diagnostics namespace and rescan only the selected folder. Only an exact signed reply from the pinned helper can mark upload observed. After an accepted upload, VaultSync authorizes exactly one signed helper response file with 256 random bytes in the same namespace; only its fresh synchronized arrival with full validation can mark download observed. Roundtrip remains unobserved. Opaque copies may remain in peers, backups, versions, conflicts, or tombstones."))
Text(L10n.tr("VaultSync will create one signed request with 256 random bytes in the already authorized diagnostics namespace and rescan only the selected folder. Only an exact signed reply from the pinned helper can mark upload observed. After an accepted upload, VaultSync authorizes exactly one signed helper response file with 256 random bytes in the same namespace; only its fresh synchronized arrival with full validation can mark download observed. A causal roundtrip is confirmed only from the same operation's upload then download and is never global sync health. Opaque copies may remain in peers, backups, versions, conflicts, or tombstones."))
}
}

Expand All @@ -100,7 +100,7 @@ struct ControlledDiagnosticsView: View {
Label(L10n.tr("Explicit local or VPN pairing only"), systemImage: "lock.shield")
Label(L10n.tr("TLS 1.3 with an exact QR-pinned key"), systemImage: "checkmark.seal")
Label(L10n.tr("No discovery, trust adoption, Relay tunnel, or automatic namespace"), systemImage: "hand.raised")
Text(L10n.tr("Pairing and capability checks create no upload, download, or roundtrip evidence. Only a separate explicit check may mark upload observed and, after it, download observed; roundtrip remains independent. The diagnostics namespace is visible to synchronized peers and may remain in backups, versions, conflict copies, and tombstones."))
Text(L10n.tr("Pairing and capability checks create no upload, download, or roundtrip evidence. Only a separate explicit check may mark upload observed, then download observed, and derive the causal roundtrip from that one operation alone. The diagnostics namespace is visible to synchronized peers and may remain in backups, versions, conflict copies, and tombstones."))
.font(.caption)
.foregroundStyle(.secondary)
} header: {
Expand Down Expand Up @@ -561,6 +561,8 @@ struct ControlledDiagnosticsView: View {
)
case .downloadObserved:
return L10n.tr("Upload and download observed — roundtrip remains unobserved")
case .roundtripConfirmed:
return L10n.tr("Causal roundtrip confirmed for this one operation — not global sync health")
default:
return L10n.tr("Partial: upload observed, download unobserved — no late result can upgrade it")
}
Expand All @@ -572,6 +574,8 @@ struct ControlledDiagnosticsView: View {
return L10n.fmt("Upload pending after %d of 8 exact polls", status.completedPolls)
case .uploadObserved, .downloadObserved:
return L10n.tr("Upload and download observed — roundtrip remains unobserved")
case .roundtripConfirmed:
return L10n.tr("Causal roundtrip confirmed for this one operation — not global sync health")
case .cancelled:
return L10n.tr("Upload check cancelled — no late result can upgrade it")
case .timedOut:
Expand All @@ -593,6 +597,7 @@ struct ControlledDiagnosticsView: View {
switch phase {
case .uploadObserved: return "arrow.up.circle.fill"
case .downloadObserved: return "arrow.down.circle.fill"
case .roundtripConfirmed: return "arrow.triangle.2.circlepath.circle.fill"
case .preflighting, .checking: return "hourglass"
case .cancelled, .timedOut, .interrupted, .unavailable: return "exclamationmark.circle"
case .conflict, .rateLimited, .unsupported: return "xmark.shield"
Expand All @@ -601,7 +606,7 @@ struct ControlledDiagnosticsView: View {

private func uploadStatusColor(_ phase: DiagnosticsPairingController.UploadPhase) -> Color {
switch phase {
case .uploadObserved, .downloadObserved: return Color.statusSuccess
case .uploadObserved, .downloadObserved, .roundtripConfirmed: return Color.statusSuccess
case .preflighting, .checking: return Color.statusAttention
case .cancelled, .timedOut, .interrupted, .unavailable: return Color.statusAttention
case .conflict, .rateLimited, .unsupported: return Color.statusError
Expand Down
Loading
Loading