GAMBIT — per-action Output schemas + AFM endpoint side-channel¶
Status: verified — 2026-04-29 (Phase D.6.6-impl Phase 1 deploy)
Empirical extraction of *ActionDeclaration.Output types for the 15
static-success actions per ADR-0009 §Consequences, plus the AFM endpoint
side-channel mechanism used by acquireDeviceUsageAssertion. Phase 1
implementation lives at:
inject/iosmux_gambit.mper-action Output dispatch (commitsb440a92→49c8f1e)inject/iosmux_gambit.mgambit_emit_afm_via_endpointAFM emit (commits1e8ef81→28d7d89)
Sources: nm -arch x86_64 | xcrun swift-demangle on CoreDevice (24026
symbols), CoreDeviceService (2753 symbols), CoreDeviceUtilities
(50391 symbols), DVTCoreDeviceCore (3481 symbols). All offsets are
file offsets in the CoreDevice x86_64 slice unless prefixed with
framework name.
TL;DR¶
GAMBIT's per-action dispatch (Phase 1 baseline) maps each of the 15 allow-listed action IDs to one of three Output container shapes:
-
_VoidBox(Void) — the empty 0-key xpc_dictionary. Used forpair,unpair,acquireusageassertion,connect,disconnect,tags,darwinnotificationobserve. Public Swift API for these actions returns(); the OutputContainer carries Void on the wire. -
_CodableBox<T>— wraps a concrete Codable struct asOutputContainer.rawValue. Used for the rest:
| Action ID | Output struct |
|---|---|
listusageassertions |
[UsageAssertionInformation] |
gettrainname |
GetTrainNameActionDeclaration.Output { name: String } |
darwinnotificationpost |
[DarwinNotificationPostActionResult] |
enableddiservices |
EnableDeveloperDiskImageResult |
disableddiservicesaction |
DeviceStateSnapshot |
fetchddimetadata |
DDIMetadata |
updatehostddis |
UpdateHostDDIsResult |
removehostddis |
RemoveHostDDIsResult |
_CodableBox<DeviceConnectionChangeResult>— for actions whose semantic Output is "connection state changed" (pair,connect,disconnect,unpair). The PairAction iter 1-17 trail (gambit-pair-action-schema.md) spelled out the byte-level schema.
Public Swift API: RemoteDevice.pair() async throws -> () returns Void
to callers but the wire OutputContainer.rawValue carries
DeviceConnectionChangeResult.
In addition, acquireDeviceUsageAssertion requires a separate
AssertionFulfilledMessage (AFM) emitted over an XPC endpoint
side-channel after the action's Void reply.
1. ActionDeclaration / OutputContainer protocol layout¶
CoreDeviceUtilities.ActionDeclaration (protocol descriptor at
CoreDeviceUtilities 0x265ee8) requires OutputContainer and
InputContainer associated types. The container's RawValue: SY
(RawRepresentable) is the semantic Output exposed to Swift callers.
Two concrete container types ship in CoreDeviceUtilities:
CoreDeviceUtilities._VoidBox(descriptor0x266028) — Codable wrapper withrawValue: (). Wire form: empty 0-key xpc_dictionary.CoreDeviceUtilities._CodableBox<A>(generic) — Codable wrapper withrawValue: A. Wire form: nested xpc_dictionary mirroringA.
The actual semantic binding for each action is determined by the
*ActionImplementation struct in CoreDeviceService and the
_Continuation<T> it awaits. CoreDeviceService continuations recovered
(addresses are file offsets in CoreDeviceService binary):
| Address | _Continuation<T> |
Adjacent action implementation |
|---|---|---|
0x...642c |
() (Void) |
AcquireDeviceUsageAssertionActionImplementation |
0x...6566 |
[UsageAssertionInformation] |
ListUsageAssertionsActionImplementation |
0x...6644 |
DeviceConnectionChangeResult |
ConnectActionImplementation |
0x...66e4 |
DeviceStateSnapshot |
DisableDeveloperDiskImageServicesActionImplementation |
0x...67e2 |
EnableDeveloperDiskImageResult |
EnableDeveloperDiskImageServicesActionImplementation |
0x...6296 |
RemoveHostDDIsResult |
RemoveHostDDIsActionImplementation |
0x...62d2 |
UpdateHostDDIsResult |
UpdateHostDDIsActionImplementation |
Pair, Unpair, Tags, Disconnect, DarwinNotificationObserve,
DarwinNotificationPost, FetchDDIMetadata implementations have no
locally-attached _Continuation<T> symbol in CDS — their Output
binding is determined by static metaclass + public-API evidence (see
§2 per-type detail).
2. Per-action Output schemas (15-action allow-list)¶
| # | Action ID | Output container | Output payload | Side events |
|---|---|---|---|---|
| 1 | pair |
_CodableBox<DeviceConnectionChangeResult> |
DCCR { outcome: .success, updatedSnapshot: <DSS> } |
None on success path; PairActionUpdate streams via update channel during action |
| 2 | unpair |
_VoidBox |
Void | None |
| 3 | acquireusageassertion |
_VoidBox |
Void | AssertionFulfilledMessage over endpoint side-channel — see §3 |
| 4 | listusageassertions |
_CodableBox<[UsageAssertionInformation]> |
array (empty [] for stub) |
None |
| 5 | connect |
_CodableBox<DeviceConnectionChangeResult> |
DCCR { outcome: .success, updatedSnapshot: <DSS connected> } |
None |
| 6 | disconnect |
_CodableBox<DeviceConnectionChangeResult> |
DCCR { outcome: .success, updatedSnapshot: <DSS disconnected> } |
None |
| 7 | tags |
undetermined — possibly _CodableBox<LabelsActionResult> |
gap | None |
| 8 | gettrainname |
_CodableBox<GetTrainNameActionDeclaration.Output> |
{ name: String } (e.g. "24A" for iOS 18) |
None |
| 9 | darwinnotificationobserve |
_VoidBox |
Void | observations stream over XPCSideChannel (Input carries observationToken) |
| 10 | darwinnotificationpost |
_CodableBox<[DarwinNotificationPostActionResult]> |
one element per posted name | None |
| 11 | enableddiservices |
_CodableBox<EnableDeveloperDiskImageResult> |
{ deviceStateSnapshot, ddiServicesInfo, metrics: nil } |
None |
| 12 | disableddiservicesaction |
_CodableBox<DeviceStateSnapshot> |
full DSS | None |
| 13 | fetchddimetadata |
_CodableBox<DDIMetadata> |
high-risk: init throws; needs proper plist keys | None |
| 14 | updatehostddis |
_CodableBox<UpdateHostDDIsResult> |
{ candidateDDIDirectories: [], currentDDIs: [], hostCoreDeviceVersion: 1.0, originalDDIs: [] } |
None |
| 15 | removehostddis |
_CodableBox<RemoveHostDDIsResult> |
{ originalDDIs: [], currentDDIs: [] } |
None |
lockstate is NOT in this allow-list (returns non-Void
LockState; static success would lie about real device state).
2.1 DeviceConnectionChangeResult¶
Used by Pair, Connect, Disconnect, Unpair (via reuse of the same
builder). Full byte-level schema in
gambit-pair-action-schema.md.
2.2 DeviceStateSnapshot¶
Used by Disable DDIServices Output and embedded in DCCR.updatedSnapshot
and EnableDeveloperDiskImageResult.deviceStateSnapshot and
AssertionFulfilledMessage.updatedSnapshot.
| Key | Type | Source |
|---|---|---|
deviceInfo |
CoreDeviceProtocols.DeviceInfo |
getter 0x29d350 |
capabilityImplementations |
[CoreDevice.Capability : [Swift.String]] |
getter 0x2cc60 |
monotonicIdentifier |
Swift.Int64 |
getter 0x29d500 |
capabilities: Set<Capability> is computed from
capabilityImplementations, NOT separately encoded.
Initializer: init(deviceInfo:, capabilityImplementations:, monotonicIdentifier:) at 0x29d520.
2.3 EnableDeveloperDiskImageResult¶
| Key | Type | Source |
|---|---|---|
deviceStateSnapshot |
DeviceStateSnapshot |
0x55a40 |
ddiServicesInfo |
DeveloperDiskImageServiceInfo |
0x55a60 |
metrics |
[Metric]? (optional) |
0x55a90 |
Init: init(deviceStateSnapshot:, ddiServicesInfo:, metrics:) at
0x55ab0. DeveloperDiskImageServiceInfo() empty init at 0x553b0.
2.4 UpdateHostDDIsResult / RemoveHostDDIsResult¶
UpdateHostDDIsResult {
candidateDDIDirectories: [URL],
currentDDIs: [DeveloperDiskImage],
hostCoreDeviceVersion: VersionNumber,
originalDDIs: [DeveloperDiskImage]
}
RemoveHostDDIsResult {
originalDDIs: [DeveloperDiskImage],
currentDDIs: [DeveloperDiskImage]
}
2.5 UsageAssertionInformation (List Usage Assertions)¶
init(identifier:owningProcess:processName:creationReason:hostName:preparednessDescription:) at 0x33be0 —
all six fields non-Optional:
| Key | Type | Source |
|---|---|---|
identifier |
Foundation.UUID |
0x3c4f0 |
owningProcess |
Swift.Int32 |
0x33970 |
processName |
Swift.String |
0x33990 |
creationReason |
Swift.String |
0x339c0 |
hostName |
Swift.String |
0x339f0 |
preparednessDescription |
Swift.String |
0x33a20 |
2.6 DDIMetadata (FetchDDIMetadata Output) — high-risk¶
10-key Codable struct. init may throw if metadata dict is missing
required DDIMetadata.PlistKeys keys (referenced at 0x87820). Empty
shape may NOT validate — gap; not exercised in Phase 1.
2.7 PairActionUpdate (NOT Output)¶
Delivered through Pair's update channel (NOT the Output channel):
.started(0x350078).challenge(Bool, Double?)(0x350074)
PairActionChallengeAnswer { pin: String } is the client-supplied
challenge response.
3. AFM endpoint side-channel mechanism¶
For acquireDeviceUsageAssertion Apple's design uses a second XPC
connection between client (devicectl/Xcode) and server (CDS), separate
from the action-XPC connection.
3.1 Wire layout¶
The AUA action's Input carries an xpc_endpoint_t:
AcquireDeviceUsageAssertionActionDeclaration.Input {
reason: Swift.String,
options: CoreDevice.UsageAssertionOptions,
endpoint: __C.OS_xpc_object // xpc_endpoint_t — mach send right
}
- Client side (CD
0x30af0..0x30b0c):xpc_endpoint_create()once per AUA call; result stored at offset 0 ofInput. - Server side (CDS
0x2120d):xpc_connection_create_from_endpoint()converts the inline endpoint into a live peer connection. CDS stores the result onInProgressServerAssertion.peerConnectionivar. - Server emit (CDS
0x1e66c): "Succesfully fulfilled assertion %s for device %s. Notifying client" — AFM emit point AFTER the action reply has been sent.
3.2 AssertionFulfilledMessage schema¶
AssertionFulfilledMessage {
identifier: Foundation.UUID, // xpc_uuid (REQUIRED)
updatedSnapshot: DeviceStateSnapshot // xpc_dict (REQUIRED)
}
Init at CD 0x2ef20. Both fields non-Optional. identifier MUST echo
the assertion UUID the client allocated client-side (carried via the
action's outer envelope, NOT via Input — Input has reason / options /
endpoint, no UUID field).
Mangled type: _$s10CoreDevice25AssertionFulfilledMessageV →
mangledTypeName: "CoreDevice.AssertionFulfilledMessage" on the wire.
3.3 CDS-side preparedness pipeline (server-side AFM emit ordering)¶
Before AFM is emitted, CDS walks each preparedness stage in order (strings at the cited CDS file offsets):
| CDS offset | Log | Stage |
|---|---|---|
0x21114 |
"Received request to create usage assertion ..." | entry |
0x196b4 |
"Received success from device fulfilling tunnel assertion request" | tunnel granted |
0x19b00 |
"Acquiring power assertion if requested in preparedness" | powerAssertionTaken |
0x1a265 |
"Checking developer mode status" | developerModeEnabledIfRequired |
0x1bdcb |
"Not enabling DDI services ... Proceeding to extended info loading" | developerDiskImageServicesEnabled |
0x1cc6a / 0x1ce5e |
"Not loading extended info ... Proceeding to notify client that the assertion was fulfilled" | extendedDeviceInfoLoaded |
0x1e66c |
"Succesfully fulfilled assertion %s for device %s. Notifying client" | AFM emit point |
CDS imports zero progress-emission symbols (no setProgress,
setUnitCountCompleted, fractionCompleted). ProgressUnit is solely
client-side accounting on the NSProgress returned by the
completion-handler API at CD 0x304a0. The server emits AFM once,
after all preparedness stages are complete.
3.4 Client-side state machine (Result wrapping)¶
RemoteDevice.acquireDeviceUsageAssertion(withReason:options:) async
throws -> DeviceUsageAssertion at CD 0x31750 is a Result-wrapping
orchestrator. Sequence:
- Build
IdentifiableAssertionDetails(with fresh client UUID). - Call
xpc_endpoint_create()(CD0x30afd). - Install unfair-locked listener consuming AFM, updating
InProgressClientAssertion.state(CD ivar0x34b9d0) to.fulfilledor.invalidated(error). - Dispatch action via
CoreDeviceUtilities.ActionDeclaration.forward(...). _swift_continuation_awaiton state-change.- On
.fulfilled: buildDeviceUsageAssertion(ObjC class), log "Successfully acquired usage assertion ..." (CD0x371930), resume.success(assertion). - On
.invalidated(err): log "Failed to acquire usage assertion on device %s due to error: %s" (CD0x3718e0), resume.failure(err).
NO timeout in this code. _swift_continuation_await blocks indefinitely.
The user-visible errorCode 3 ~10 ms after the success reply originates
in the assertionq dispatcher chain in CoreDevice itself when the
side-channel peer connection invalidates — see
aua-side-channel-mechanism.md for the
full failure path.
3.5 _shadowUseAssertion setter (Xcode side)¶
DVTCoreDeviceCore's DVTCoreDevice_Impl private class sets
_shadowUseAssertion: DeviceUsageAssertion? (field-offset symbol Wvd at
0x6d7f8) when the AUA Result resolves to .success. This is the
ObjC-class instance the hasConnection / Pair-button-rendering UI gate
reads from — see
pair-button-and-cfnetwork.md §"Pair
button gating".
DeviceUsageAssertion ivars: identifier, options, reason,
_lockedMutableState. State enum: .fulfilled, .invalidated(Error?).
The instance is built client-side from the values the client itself
sent in Input.reason / Input.options plus a UUID it picked. The
server only signals "assertion now fulfilled" via AFM; it does NOT send
back the assertion fields.
4. RDSUE event chain (Q-D66-14 anchor)¶
Used by GAMBIT Phase 1 to flip kvoCache_isPaired and clear the Xcode
spinner gate (commit 648f531).
Inject emits RDSUE
→ Mercury XPC delivers as ServiceEvent
→ CoreDevice EventManager.receiveEvent (file offset 0x306ff0)
→ typed handler for ServiceEventKind.remoteDeviceStateUpdate
→ DeviceManager.handle(deviceUpdateSnapshot:) (thunk 0xcc4a0)
→ RemoteDevice.updateState(from:markServiceConnected:) (thunk 0x183590)
→ _stateStorage setter
→ addDeviceInfoChanged callback fires
→ DVTCoreDeviceCore._updatePropertiesImpactingAvailability
→ kvoCache_isPaired := pairingState.toBool()
4.1 Critical filter: monotonicIdentifier¶
RemoteDevice.updateState silently drops snapshots whose
DeviceStateSnapshot.monotonicIdentifier: Int64 is ≤ the previously
cached value. NO decoder error, NO log, just a stale-event filter.
GAMBIT seeds its monotonic counter from mach_absolute_time() at
install time (~10^11 baseline) so its broadcasts are guaranteed strictly
newer than Apple's session-local counter.
4.2 RemoteDeviceStateUpdatedEvent Codable schema¶
RemoteDeviceStateUpdatedEvent {
deviceIdentifier: CoreDevice.DeviceIdentifier,
state: CoreDevice.DeviceStateSnapshot
}
init at CD 0x29bb50. static var event: ServiceEventKind {
.remoteDeviceStateUpdate } getter at 0x57380. Codable keys at
0x393680.
4.3 ServiceEventKind raw values¶
(__cstring at CD 0x36ff80..0x3700a0):
remoteDeviceStateUpdate→RemoteDeviceStateUpdatedEvent— pushes RDSUE streamdeviceManagerCheckInComplete→DeviceManagerCheckInCompleteEvent— initial bulkdeviceManagerFullyInitialized→DeviceManagerFullyInitializedEvent— readiness gate, no payloaddeviceManagerDevicesUpdate→ device-set deltaoperationStateUpdate→OperationStateUpdatedEvent— operation manager (observational, not blocking)pairingEvent,serviceOperationManagerCheckInComplete,deviceStateChanged,deviceRequestUpdate,operationStatusChanged— Codable types not in this binary's__text
GAMBIT only emits remoteDeviceStateUpdate (RDSUE) and
AssertionFulfilledMessage (AFM). Other events are observational and
empirically not required for action flow.
5. DevicePreparedness OptionSet bits¶
DevicePreparedness (Swift OptionSet, RawValue: Int)
developerModeEnabledIfRequired = 0x1 (CD 0x1a78a0)
developerDiskImageServicesEnabled = 0x2 (CD 0x1a78b0)
extendedDeviceInfoLoaded = 0x4 (CD 0x1a78c0)
powerAssertionTaken = 0x8 (CD 0x1a0c00)
all = 0xf (CD 0x1a78d0; bitwise OR of all four)
Encoded via singleValueContainer as Swift.Int64 (NOT
{rawValue: N} dict). Echoing requiredPreparedness from the request
is sufficient — Xcode typically requests 0xf (all).
6. ProgressUnit raw values (AcquireDeviceUsageAssertion)¶
Decoder at CD 0x32fd0 uses lookup table 0x0000040302010005 (bytes
at shift rawValue*8). Mapping:
| rawValue | Case |
|---|---|
| 1 | started |
| 2 | createdTunnelConnection |
| 3 | enablingDeveloperDiskImageServices |
| 4 | fetchingExtendedDeviceInfo |
| 5 | completed |
| ≥6 or 0 | none (Optional sentinel) |
Server emits these only via NSProgress increments client-side; not
required on the wire by GAMBIT.
7. Open questions / gaps¶
These remain after Phase 1 deploy. None block the cosmetic-accepted Q-D66-15 baseline.
- TagsAction Output — undetermined. Possibly
LabelsActionResult(Tags ≈ Labels). No_Continuation<T>nearTagsActionImplementation, no.Outputnominal type descriptor. - DisconnectAction Output — strongly assumed
DCCR, no direct continuation evidence.DisconnectActionImplementationbody at0x...677a(CDS) would resolve. - DDIMetadata empty Output —
init(metadata: [:])may throw; minimum-viable shape needsDDIMetadata.PlistKeysenumerated. _shadowUseAssertionMercury wire path — AFM event delivery channel is the endpoint connection (§3.1); whether it routes through the same RemoteXPC stream that carries action replies, or a completely separate XPC connection, is the open question. The current GAMBITgambit_emit_afm_via_endpointopens a fresh outboundxpc_connection_create_from_endpointper action and emits over that. Empirically the Apple side expects an inbound persistent peer to live for the assertion's lifetime — the cosmetic gap in Q-D66-15 originates here.outcome.failurepayload —Mercury.NSErrorContainer(CD0x34c718) is the failure carrier. GAMBIT only emits success; if a real failure path were ever needed,NSErrorContainerschema would need its own probe.
See also¶
gambit-pair-action-schema.md— byte-level schema forDeviceConnectionChangeResult(Pair / Connect / Disconnect / Unpair Output).mercury-action-interceptor-design.md— GAMBIT hook target + 15-action allow-list.mercury-envelope-empirical.md— Mercury action invocation envelope (whereInput.endpointarrives).aua-side-channel-mechanism.md— why the AFM endpoint side-channel fires the cosmetic Q-D66-15 errorCode 3 path.pair-button-and-cfnetwork.md—_shadowUseAssertionUI binding.- ADR-0009 §Consequences — the 15-action allow-list.
- ADR-0006 — empirical discipline backing every offset / type cited above.