Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
81bdb30
test(iroh): verify relay credential rollover
Jul 19, 2026
5e91dbc
fix(iroh): observe relay expiry closure directly
Jul 19, 2026
9b133f8
fix(iroh): bound release gate observation
Jul 19, 2026
1e41695
fix(iroh): isolate release gate RPC accessors
Jul 19, 2026
0f5a38d
test(iroh): expose deferred continuity gap
Jul 19, 2026
f3bd98b
fix(iroh): forward deferred continuity evidence
Jul 19, 2026
416a49e
refactor(iroh): isolate artifact gate preparation
Jul 19, 2026
01b0b40
test(iroh): expose artifact completion race
Jul 19, 2026
52b335b
fix(iroh): await artifact readiness before rollover
Jul 19, 2026
d0eb7fc
test(iroh): expose attach readiness deadline gap
Jul 19, 2026
8d585d3
fix(iroh): make attach readiness gate observable
Jul 19, 2026
4ba8b71
refactor(iroh): isolate auth reconcile policy
Jul 19, 2026
6a8dbaf
test(iroh): expose duplicate auth activation race
Jul 19, 2026
f2bbc8a
fix(iroh): preserve same-account activation ownership
Jul 19, 2026
45824f3
refactor(iroh): expose initial relay readiness policy
Jul 19, 2026
159737b
test(iroh): expose relay-only startup retry gap
Jul 19, 2026
9a4352f
fix(iroh): await required relay credentials on startup
Jul 19, 2026
c806bf5
test(iroh): expose artifact scan visibility gap
Jul 20, 2026
192ea04
fix(iroh): publish artifact path before readiness scan
Jul 20, 2026
a327baa
test(iroh): expose canonical artifact path alias
Jul 20, 2026
aa74f7a
fix(iroh): recognize canonical macOS temp paths
Jul 20, 2026
c783a9c
chore(iroh): consume home relay continuity release
Jul 20, 2026
009a5eb
test(iroh): expose duplicate discovery rate-limit outage
Jul 20, 2026
b2899a1
fix(iroh): reuse verified discovery for first dial
Jul 20, 2026
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
8 changes: 5 additions & 3 deletions .github/workflows/iroh-release-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ on:
- all
- automatic
- relay-only
- relay-expiry
- direct-only
- private-path

Expand Down Expand Up @@ -58,7 +59,7 @@ jobs:
strategy:
fail-fast: false
matrix:
mode: ${{ fromJSON(inputs.mode == 'all' && '["automatic","relay-only","direct-only","private-path"]' || format('["{0}"]', inputs.mode)) }}
mode: ${{ fromJSON(inputs.mode == 'all' && '["automatic","relay-only","relay-expiry","direct-only","private-path"]' || format('["{0}"]', inputs.mode)) }}
runs-on: ${{ vars.MACOS_RUNNER_STREAMED_VALIDATION || 'warp-macos-15-arm64-6x' }}
timeout-minutes: 120
steps:
Expand All @@ -78,13 +79,13 @@ jobs:
xcrun simctl list runtimes available | grep -Eq '\biOS\b' || xcodebuild -downloadPlatform iOS

- name: Install app build dependencies
if: ${{ matrix.mode == 'automatic' || matrix.mode == 'relay-only' }}
if: ${{ matrix.mode == 'automatic' || matrix.mode == 'relay-only' || matrix.mode == 'relay-expiry' }}
run: |
./scripts/install-zig-ci.sh
./scripts/download-prebuilt-ghosttykit.sh || ./scripts/ensure-ghosttykit.sh

- name: Materialize isolated staging account
if: ${{ matrix.mode == 'automatic' || matrix.mode == 'relay-only' }}
if: ${{ matrix.mode == 'automatic' || matrix.mode == 'relay-only' || matrix.mode == 'relay-expiry' }}
env:
CMUX_DOGFOOD_STACK_EMAIL: ${{ secrets.CMUX_DOGFOOD_STACK_EMAIL }}
CMUX_DOGFOOD_STACK_PASSWORD: ${{ secrets.CMUX_DOGFOOD_STACK_PASSWORD }}
Expand All @@ -105,6 +106,7 @@ jobs:
case "${{ matrix.mode }}" in
automatic) TAG=irgaut ;;
relay-only) TAG=irgrel ;;
relay-expiry) TAG=irgexp ;;
direct-only) TAG=irgdir ;;
private-path) TAG=irgprv ;;
esac
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,41 @@ public protocol CmxByteTransport: Sendable {
func close() async
}

/// Optional privacy-safe identity for the exact native connection underneath a
/// byte transport.
///
/// Release gates use this to prove that credential refresh did not replace a
/// live transport. The value is process-local and must never be persisted or
/// sent to a server.
public protocol CmxByteTransportContinuityIdentifying: CmxByteTransport {
/// Returns a process-local identifier for the currently connected native
/// transport, or `nil` before connection or after teardown.
func transportContinuityID() async -> UInt64?
}

/// A privacy-safe handle that waits for one exact native transport to close.
///
/// The handle captures the transport generation at creation time, so callers
/// can retain it across owner teardown without accidentally observing a later
/// replacement connection.
public struct CmxTransportClosureObservation: Sendable {
private let waitUntilClosedOperation: @Sendable () async -> Void

public init(waitUntilClosed: @escaping @Sendable () async -> Void) {
self.waitUntilClosedOperation = waitUntilClosed
}

public func waitUntilClosed() async {
await waitUntilClosedOperation()
}
}

/// Optional close notification for the exact native transport currently
/// installed underneath a byte transport.
public protocol CmxByteTransportClosureObserving: CmxByteTransport {
func transportClosureObservation() async -> CmxTransportClosureObservation?
}

/// Independently framed server-event bytes delivered outside the RPC control stream.
public typealias CmxIndependentEventByteStream = AsyncThrowingStream<Data, any Error>

Expand Down
6 changes: 3 additions & 3 deletions Packages/Shared/CmuxIrohTransport/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Packages/Shared/CmuxIrohTransport/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let package = Package(
.package(path: "../CMUXMobileCore"),
.package(
url: "https://github.com/manaflow-ai/iroh-ffi.git",
exact: "1.0.2-cmux.3"
exact: "1.0.2-cmux.4"
),
],
targets: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ extension CmxIrohClientRuntime {
localBindingExpectation: policy.expectation,
managedRelayURLs: managedRelayURLs,
allowedRouteRelayURLs: endpointRelayProfile.allowedRelayURLs,
offlinePolicy: offlinePolicy
offlinePolicy: offlinePolicy,
verifiedDiscovery: policy.discovery
)
provider = registryContextProvider
} else {
Expand All @@ -171,6 +172,7 @@ extension CmxIrohClientRuntime {
offlinePolicy: offlinePolicy,
lanFallback: lanFallback,
customPrivateFallback: customPrivateFallback,
verifiedDiscovery: policy.discovery,
now: now
)
registryContextProvider = provider
Expand All @@ -194,6 +196,7 @@ extension CmxIrohClientRuntime {
broker: broker,
managedRelayURLs: managedRelayURLs,
selectedRelayURLs: endpointRelayProfile.allowedRelayURLs,
automaticRefreshEnabled: automaticRelayCredentialRefreshEnabled,
credentialDidInstall: { [handleRelayCredential] response in
await handleRelayCredential(response, policy.binding)
}
Expand All @@ -203,13 +206,17 @@ extension CmxIrohClientRuntime {

let bootstrap = startRelays ? configuration.cachedRelayCredential : nil
if startRelays || bootstrap != nil {
let requiresRelayReadiness = !protocolConfiguration
.allowsNATTraversalAfterAdmission
do {
try await coordinator.activate(
bindingID: policy.binding.bindingID,
endpointIdentity: policy.binding.endpointID,
bootstrap: bootstrap
bootstrap: bootstrap,
waitForInitialCredential: requiresRelayReadiness
)
} catch {
if requiresRelayReadiness { throw error }
// Registration remains authoritative; direct paths remain usable.
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ extension CmxIrohClientRuntime {
broker: broker,
managedRelayURLs: replacementManagedURLs,
selectedRelayURLs: profile.allowedRelayURLs,
automaticRefreshEnabled: automaticRelayCredentialRefreshEnabled,
credentialDidInstall: { [handleRelayCredential] response in
await handleRelayCredential(response, binding)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public actor CmxIrohClientRuntime {
let lanFallback: LANFallbackProvider?
let customPrivateFallback: CustomPrivateFallbackProvider?
let now: @Sendable () -> Date
let automaticRelayCredentialRefreshEnabled: Bool
let handleBinding: BindingHandler
let handleCachedBindings: CachedBindingsHandler
let handleRelayCredential: RelayCredentialHandler
Expand Down Expand Up @@ -136,6 +137,7 @@ public actor CmxIrohClientRuntime {
lanFallback: LANFallbackProvider? = nil,
customPrivateFallback: CustomPrivateFallbackProvider? = nil,
now: @escaping @Sendable () -> Date = { Date() },
automaticRelayCredentialRefreshEnabled: Bool = true,
handleBinding: @escaping BindingHandler = { _, _ in true },
handleCachedBindings: @escaping CachedBindingsHandler = { _, _ in },
handleRelayCredential: @escaping RelayCredentialHandler = { _, _ in },
Expand Down Expand Up @@ -175,6 +177,7 @@ public actor CmxIrohClientRuntime {
self.lanFallback = lanFallback
self.customPrivateFallback = customPrivateFallback
self.now = now
self.automaticRelayCredentialRefreshEnabled = automaticRelayCredentialRefreshEnabled
self.handleBinding = handleBinding
self.handleCachedBindings = handleCachedBindings
self.handleRelayCredential = handleRelayCredential
Expand All @@ -188,6 +191,12 @@ public actor CmxIrohClientRuntime {
currentSnapshot
}

/// Returns the non-secret hard expiry of the relay credential currently
/// installed on the live endpoint.
public func relayCredentialExpiresAt() async -> Date? {
await relayCoordinator?.credentialExpiresAt()
}

/// Monotonic count of online broker snapshots verified by this runtime.
public func liveDiscoverySnapshotGeneration() -> UInt64 {
liveDiscoveryGeneration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@ public actor CmxIrohClientSession {
return await connection.isClosed()
}

/// Returns Iroh's process-local identity for this exact admitted QUIC
/// connection. Alternate endpoint implementations may not provide one.
func connectionContinuityID() async -> UInt64? {
guard !closed,
let connection,
let continuityConnection = connection as? any CmxIrohConnectionContinuityIdentifying else {
return nil
}
guard !(await connection.isClosed()) else { return nil }
return await continuityConnection.connectionContinuityID()
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
/// Reads package-private path evidence from the exact admitted connection.
func observedSelectedPath() async -> CmxIrohObservedConnectionPath {
guard let connection = connection as? any CmxIrohConnectionPathInspecting else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// Package-private access to Iroh's process-local stable connection identity.
///
/// Keeping this separate from ``CmxIrohConnection`` means alternate endpoint
/// implementations do not need to expose a native diagnostic handle.
protocol CmxIrohConnectionContinuityIdentifying: Sendable {
func connectionContinuityID() async -> UInt64
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import CMUXMobileCore
import Foundation

/// Defers transport construction until the signed-in runtime finishes activation.
actor CmxIrohDeferredByteTransport: CmxByteTransport {
actor CmxIrohDeferredByteTransport:
CmxByteTransport,
CmxByteTransportClosureObserving,
CmxByteTransportContinuityIdentifying
{
private let request: CmxByteTransportRequest
private let provider: any CmxIrohDeferredTransportProviding
private var connectTask: Task<any CmxByteTransport, any Error>?
Expand Down Expand Up @@ -79,4 +83,18 @@ actor CmxIrohDeferredByteTransport: CmxByteTransport {
transport = nil
await closing?.close()
}

func transportContinuityID() async -> UInt64? {
guard let identifying = transport as? any CmxByteTransportContinuityIdentifying else {
return nil
}
return await identifying.transportContinuityID()
}

func transportClosureObservation() async -> CmxTransportClosureObservation? {
guard let observing = transport as? any CmxByteTransportClosureObserving else {
return nil
}
return await observing.transportClosureObservation()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,8 @@ public actor CmxIrohHostRuntime {
try await relayCoordinator.activate(
bindingID: policy.binding.bindingID,
endpointIdentity: endpointID,
bootstrap: policy.relayBootstrap
bootstrap: policy.relayBootstrap,
waitForInitialCredential: true
)
}
try requireCurrent(revision)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import CMUXMobileCore
import Foundation
import IrohLib

struct CmxIrohLibConnection: CmxIrohConnection, CmxIrohConnectionPathInspecting {
struct CmxIrohLibConnection:
CmxIrohConnection,
CmxIrohConnectionContinuityIdentifying,
CmxIrohConnectionPathInspecting
{
let driver: Connection
let peerIdentity: CmxIrohPeerIdentity

Expand All @@ -15,6 +19,10 @@ struct CmxIrohLibConnection: CmxIrohConnection, CmxIrohConnectionPathInspecting
peerIdentity
}

func connectionContinuityID() async -> UInt64 {
driver.stableId()
}

func observedSelectedPath() async -> CmxIrohObservedConnectionPath {
CmxIrohObservedConnectionPath(
snapshots: driver.paths().map(CmxIrohConnectionPathSnapshot.init)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import CMUXMobileCore
import Foundation

/// Projects a pooled admitted session's control lane through the legacy byte seam.
actor CmxIrohPooledByteTransport: CmxByteTransport {
actor CmxIrohPooledByteTransport:
CmxByteTransport,
CmxByteTransportClosureObserving,
CmxByteTransportContinuityIdentifying
{
private let request: CmxByteTransportRequest
private let pool: CmxIrohClientSessionPool
private let ownerID = UUID()
Expand Down Expand Up @@ -73,6 +77,17 @@ actor CmxIrohPooledByteTransport: CmxByteTransport {
)
}

func transportContinuityID() async -> UInt64? {
await session?.connectionContinuityID()
}

func transportClosureObservation() -> CmxTransportClosureObservation? {
guard let session else { return nil }
return CmxTransportClosureObservation {
await session.waitUntilClosed()
}
}

private func releaseOwnedControlSession(
reason: DiagnosticSessionLifecycleKind,
failure: DiagnosticFailureKind
Expand Down
Loading