Skip to content

Session device requirements

Status: bounded provider contract milestone. This module is pure TypeScript. It does not read platform catalogs, allocate devices, or expose daemon tools.

Consumer contract

SessionDeviceRequirement describes only device intent:

type SessionDeviceRequirement =
  | {
      schemaVersion: 1;
      platform: "ios";
      hardware: IosHardwareSelector;
      os: OsSelector;
    }
  | {
      schemaVersion: 1;
      platform: "android";
      hardware: AndroidHardwareSelector;
      os: OsSelector;
      deviceProfile?: {
        manufacturerProfile: ManufacturerProfileId;
        requires?: NonEmulatableCapability[];
      };
    }
  | {
      schemaVersion: 1;
      platform: "watchos";
      spec: { runtime: string; deviceType: string };
    };

There are no slot, lane, capacity, pool, or prior-device inputs. Those are provider-owned allocation details. Runtime parsing rejects unknown fields so an old allocation concept cannot accidentally become part of the versioned request.

iOS hardware has four explicit selector kinds:

  • exact-model carries a namespaced product-model code, CoreSimulator device type identifier, or configured exact alias;
  • model-type carries structured membership such as iPhone Pro, iPhone Pro Max, iPhone Air, or an iPad family and optional size;
  • archetype is phone, tablet, or duo;
  • omitted requests the configured default archetype.

Android hardware is phone, tablet, foldable, or omitted. Exact Android profiles and other categories are not accepted at this boundary. Hosts map each archetype through defaults.android.hardware.profiles and classify its installed profile in androidProfileArchetypes; a foldable never falls back to a phone. An unmapped or uninstalled foldable profile is refused with non-retryable unsupported_hardware. Each profile mapping may set memoryMb (an integer of at least 2048 MB), carried into AVD creation, binding fingerprints and pool matching. Fold profiles default to about 11.4 GiB, so a RAM override bounds guest memory on shared hosts. Omission preserves the existing unconstrained AVD RAM behavior; idle resources retain their override identity. Foldable release reset restores opened posture and a 180-degree hinge angle after the reset strategy and before final health verification; failure keeps the device out of the clean pool.

An Android request may name an OEM manufacturer profile as deviceProfile (owner decision on #14058). It is a simulated effect, not a firmware guarantee: the profile’s keyboard, display and background-kill behaviour are applied to the session’s stock emulator after readiness, and nothing about the OEM’s firmware, SystemUI or vendor services is present. It is not a hardware selector: it never chooses, matches or fingerprints pool devices, and it is distinct from the resolved hardware profile.profileId. The kill facet needs the app it targets, so it is deferred until the caller applies it. Releasing the session restores everything the profile changed, keyboard included. requires lists OEM capabilities the task needs; one the profile cannot emulate refuses the request with non-retryable requires_real_device before anything is reserved. Selecting a matching hardware profile or emulator skin, and routing to real OEM devices, are not part of this boundary yet.

A watchOS request names one exact Apple Watch simulator: spec is provisionDevice’s watch spec (a watchOS CoreSimulator runtime and an Apple Watch device type, nothing else). It has no hardware or OS selector and no host defaults, so normalizeSessionDeviceRequirement refuses it; normalizeWatchSessionDeviceRequirement validates it against the watch catalog and yields the exact profile { platform: "watchos", metadataState: "current", runtime, deviceType }, and matchWatchSessionDevice accepts only a pooled watch with the same runtime and device type. See docs/decisions/watchos-platform.md for the broker behaviour.

OS selectors are exact, major, minimum, and omitted. Their kind is always explicit. Values are positive dotted numeric releases. Android API levels, package revisions, letter releases, and QPR labels are not interpreted as release versions.

Numeric OS semantics

Numeric components compare without floating-point coercion. Leading zeroes and trailing zero components normalize away:

Predicate Candidate Result
exact 27.0 27.0.0 match
exact 27.0 27.0.1 mismatch
exact 27.0 27.1 mismatch
major 27 27.9.4 match
major 27 28.0 mismatch
minimum 27.0.1 28 match

An omitted OS selector becomes the configured predicate before pool matching. It never follows the active SDK or newest catalog entry implicitly.

Android releases retain apiLevel separately from osVersion. Each catalog release states whether source metadata proves major, minor, or patch precision. An exact request that needs finer evidence returns unsupported_version_precision; API level or package revision cannot fill the gap.

Resolved and pooled Android profiles retain that precision. Reuse applies the same evidence check as creation: a major-only observation cannot prove an exact minor release or a minimum within that major, even if a supplied version string includes a finer component. A later proven major can satisfy that minimum.

Catalog adapter boundary

Native adapters project source-backed facts into IosSessionDeviceCatalog or AndroidSessionDeviceCatalog. The resolver performs no I/O.

An iOS model contains both namespaces:

{
  simulatorDeviceTypeIdentifier: string;
  modelIdentifier?: string;
  name: string;
  productFamily: string;
  aliases?: readonly string[];
  archetype?: "phone" | "tablet" | "duo";
  modelType?: IosModelType;
  metadataState: "current" | "stale";
  minOsVersion?: string;
  maxOsVersion?: string | null;
}

name and productFamily are diagnostics, not matching inputs. Exact aliases use equality and must resolve to one model. Missing, ambiguous, stale, or unclassified model metadata cannot satisfy a predicate. No substring turns Pro into Pro Max, infers an iPad size, or classifies a future iPhone as phone. A requested iPad size matches exactly; an omitted size accepts any size in the requested iPad family.

iOS releases carry runtime identifier and numeric OS version. Android catalogs carry classified phone/tablet/foldable profile identifiers and releases with system image package, OS version, evidence precision, API level, ABI, and image flavor. ABI and flavor therefore remain explicit provider policy in the resolved profile.

Catalog provenance includes source, revision, and availability state. An unavailable catalog is a transient catalog_unavailable result, never evidence that hardware or a runtime is permanently unsupported.

WatchSessionDeviceCatalog lists installed watchOS runtimes (identifier, OS version, availability) and Apple Watch device types from WatchSimulatorInventory.readCatalog. A host without simctl has state not-installed, a permanent unsupported_hardware refusal; a failed read is unavailable. Neither is reported as an empty ready catalog. An unlisted device type is unsupported_hardware; an unlisted or unavailable runtime is missing_installed_runtime; both messages name the available identifiers.

Defaults and pool-first resolution

Defaults form a typed, acyclic hierarchy because each edge can only descend one level:

iOS omitted -> archetype -> model type -> exact catalog model
Android omitted -> archetype -> exact installed emulator profile

An explicit archetype starts at the archetype level. An explicit model type skips the archetype default. An explicit model skips both preceding levels. Missing, duplicate, conflicting, unknown, or stale defaults return typed configuration or catalog errors.

Normalization applies only the omitted hardware archetype and omitted OS predicate. A pool can therefore satisfy an explicit coarse phone request with any current, classified phone that meets the OS predicate. It does not have to equal the model used for a new creation. On a pool miss, resolveSessionDeviceProfile descends the remaining hardware defaults to one exact creation profile.

For major and minimum OS creation, resolution first uses the configured preferred release when it is installed and satisfies the predicate. Otherwise it chooses the highest satisfying installed release. Stable runtime/package identity breaks equal-version ties, so catalog order never changes the result. Exact predicates consider only numerically equal releases.

Known iOS model/runtime bounds can prove runtime_incompatible. The resolver does not replace an exact model or exact/default-exact OS to repair the pair. Absent compatibility bounds produce compatibility: "unknown" so the native creation authority can attempt the preserved exact pair. Malformed bounds are invalid catalog metadata.

Duo policy

Duo is a recognized iOS archetype and model type. It is distinct from phone, including when a catalog name or identifier begins with iPhone.

Duo is detected by the .iPhone-Duo simulator device-type identifier suffix, because Xcode reports its product family as iPhone. The catalog, inventory form factor, display panels and setPosture share that one predicate, and inventories report a Duo as foldable.

Execution is a host capability, capabilities.iosDuo in the session broker host configuration, and is off by default. While it is off, normalization returns non-retryable capability_disabled for an explicit Duo archetype, Duo model type, exact model classified as Duo, omitted default archetype of Duo, or known Duo model farther down the applicable default path, and resolution refuses any model that resolves to Duo. These checks occur before pool matching or creation resolution, so a refused request needs no installed Duo model, runtime, or creation default.

When it is on, Duo requests resolve like any other iOS hardware and need an iOS 27.1+ runtime. Enabling it never widens other selectors: a configured default must belong to the requested model type, so a phone or tablet request cannot resolve to a Duo. The policy is checked only at admission. A stored Duo resource stays readable for recovery, baseline reset and destroy after the capability is turned off. A broker with the capability enabled advertises session-broker/ios-duo/v1 from daemon/capabilities.

Pure API and provenance

The public module exports:

parseSessionDeviceRequirement(input);
normalizeSessionDeviceRequirement(request, defaults, catalog, capabilities?);
matchSessionDevice(normalized, candidate);
resolveSessionDeviceProfile(normalized, defaults, catalog, capabilities?);

Parsing and resolution return RequirementResult<T>, with a typed error code instead of collapsing malformed input, disabled capability, unavailable catalog, missing runtime, incompatible pair, and capacity into one result. Capacity remains outside this pure milestone.

Normalized requirements retain the original request, effective pool predicates, default-policy version, catalog source/revision, and which omitted dimensions were defaulted. Resolved profiles add exact hardware/runtime identity, the hardware default path, and whether OS resolution used exact, preferred, or highest-satisfying selection. A broker can attach this value to a fresh session while keeping allocation records and lifecycle journals private.