refactor(runtime): own provider-device admission behind a typed capability (#2556)

Ten daemon files imported isActiveProviderDevice from src/provider-device-runtime.ts,
so the daemon read provider runtime ownership mechanics directly from twelve sites
(ten daemon, one daemon runtime composition, one src/core).

The daemon now consumes a named capability: src/daemon/provider-device-admission.ts
declares ProviderDeviceAdmission with the one fact the daemon decides on, defaults to
the no-provider state every un-composed process already sees, and is installed by root
composition where the provider request providers are already composed. The ten leaf
call sites change only their import specifier; the predicate keeps its name, its
per-call read, and the request-scoped ALS behaviour underneath it.

src/core/interactors.ts keeps its edge: it also needs getProviderDeviceInteractor and
sits below the daemon, so it cannot consume the daemon's seam.

Part of #2541
This commit is contained in:
Michał Pierzchała
2026-09-13 17:00:27 +02:00
committed by GitHub
parent 4e8f367321
commit b790279bf1
26 changed files with 178 additions and 25 deletions
@@ -0,0 +1,40 @@
import { afterEach, beforeEach, expect, test } from 'vitest';
import type { DeviceInfo } from '@agent-device/kernel/device';
import {
installProviderDeviceAdmission,
isActiveProviderDevice,
providerDeviceAdmission,
} from '../provider-device-admission.ts';
const providerDevice = { id: 'provider-1', name: 'Cloud iPhone' } as unknown as DeviceInfo;
const localDevice = { id: 'local-1', name: 'iPhone 16' } as unknown as DeviceInfo;
let previous = providerDeviceAdmission();
beforeEach(() => {
previous = providerDeviceAdmission();
});
afterEach(() => {
installProviderDeviceAdmission(previous);
});
test('an un-composed process treats every device as local', () => {
installProviderDeviceAdmission({ isActive: () => false });
expect(isActiveProviderDevice(providerDevice)).toBe(false);
expect(isActiveProviderDevice(localDevice)).toBe(false);
});
test('the installed admission is what the daemon decides on', () => {
installProviderDeviceAdmission({ isActive: (device) => device.id === 'provider-1' });
expect(isActiveProviderDevice(providerDevice)).toBe(true);
expect(isActiveProviderDevice(localDevice)).toBe(false);
});
test('the fact is read per call, so a request-scoped scope stays live', () => {
let owned = false;
installProviderDeviceAdmission({ isActive: () => owned });
expect(isActiveProviderDevice(providerDevice)).toBe(false);
owned = true;
expect(isActiveProviderDevice(providerDevice)).toBe(true);
});
@@ -16,7 +16,15 @@ import {
import type { SessionState } from '../session-state.ts';
import { LeaseRegistry } from '../lease-registry.ts';
import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts';
import { createProviderDeviceRuntimeRequestProviders } from '../../provider-device-runtime.ts';
import {
createProviderDeviceRuntimeRequestProviders,
isActiveProviderDevice,
} from '../../provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../provider-device-admission.ts';
// Root composition installs the daemon's provider-device admission; this test composes the
// request providers the same way, so it installs the fact the same way.
installProviderDeviceAdmission({ isActive: isActiveProviderDevice });
import type { ProviderDeviceRuntime } from '@agent-device/contracts/device';
import { makeTestScreenRecordingResource } from '../../__tests__/test-utils/screen-recording-live-handle.ts';
import { androidObservation } from '../../platform-runtime.ts';
@@ -12,7 +12,7 @@ import {
snapshotRuntimeOperationFacts,
} from '@agent-device/contracts/snapshot-runtime';
import { deviceShape, type DeviceInfo } from '@agent-device/kernel/device';
import { isActiveProviderDevice } from '../../provider-device-runtime.ts';
import { isActiveProviderDevice } from '../provider-device-admission.ts';
import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts';
import { unavailableDeviceRuntimeGateway } from './test-device-runtime-gateway.ts';
@@ -8,7 +8,7 @@ import {
} from '../session-device-resolution.ts';
import { appleSessionObservation } from '../../platform-runtime-apple-resources.ts';
import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve';
import { isActiveProviderDevice } from '../../provider-device-runtime.ts';
import { isActiveProviderDevice } from '../provider-device-admission.ts';
import { ensureDeviceReady } from '../device-ready.ts';
vi.mock('../../platform-runtime-apple-resources.ts', async (importOriginal) => ({
@@ -18,7 +18,7 @@ vi.mock('../../platform-runtime-apple-resources.ts', async (importOriginal) => (
vi.mock('@agent-device/device-selection/dispatch-resolve', () => ({
resolveTargetDevice: vi.fn(),
}));
vi.mock('../../provider-device-runtime.ts', () => ({
vi.mock('../provider-device-admission.ts', () => ({
isActiveProviderDevice: vi.fn(() => false),
}));
vi.mock('../device-ready.ts', () => ({
@@ -33,7 +33,7 @@ import {
import { applePlugin } from '@agent-device/platform-apple';
import { type DispatchContext } from '../../core/dispatch-context.ts';
import { getRequestSignal } from '@agent-device/host-kit/request';
import { isActiveProviderDevice } from '../../provider-device-runtime.ts';
import { isActiveProviderDevice } from '../provider-device-admission.ts';
import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts';
import { unavailableDeviceRuntimeGateway } from './test-device-runtime-gateway.ts';
import { writeSolidPng } from './screenshot-runtime-fixture.ts';
+1 -1
View File
@@ -1,6 +1,6 @@
import type { AndroidObservationAdapter } from '@agent-device/contracts/android-observation';
import { AppError } from '@agent-device/kernel/errors';
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
import { isActiveProviderDevice } from './provider-device-admission.ts';
import type { SessionState } from './session-state.ts';
export type AndroidEscapeSurface = {
+1 -1
View File
@@ -10,7 +10,7 @@ import { centerOfRect, type SnapshotNode } from '@agent-device/kernel/snapshot';
import { isSnapshotNodeInteractionBlocked } from '@agent-device/capture-kit/snapshot-occlusion';
import { expireRefFrame } from './ref-frame.ts';
import type { SessionState } from './session-state.ts';
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
import { isActiveProviderDevice } from './provider-device-admission.ts';
const ANDROID_BLOCKING_MODAL_PATTERN = /\bis(?:n(?:'|'|')?t| not)\s+responding\b/i;
const ANDROID_CLOSE_APP_PATTERN = /^close app$/i;
+1 -1
View File
@@ -1,6 +1,6 @@
import type { DeviceInfo } from '@agent-device/kernel/device';
import { ensureLocalPlatformDeviceReady } from '../platform-runtime-device-ready.ts';
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
import { isActiveProviderDevice } from './provider-device-admission.ts';
import { createTtlMemo } from '@agent-device/kernel/ttl-memo';
// Exported so unit tests can assert TTL behavior without duplicating the value.
+1 -1
View File
@@ -1,6 +1,6 @@
import { isIosFamily } from '@agent-device/kernel/device';
import type { SnapshotNode } from '@agent-device/kernel/snapshot';
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
import { isActiveProviderDevice } from './provider-device-admission.ts';
import { isPostGestureStabilizationPending } from './deferred-interaction-outcome.ts';
import type { SessionState } from './session-state.ts';
import { readSimpleSelectorTarget } from '@agent-device/selectors';
@@ -1,6 +1,6 @@
import { beforeEach, expect, test, vi } from 'vitest';
import type { DeviceInfo } from '@agent-device/kernel/device';
import { isActiveProviderDevice } from '../../../provider-device-runtime.ts';
import { isActiveProviderDevice } from '../../provider-device-admission.ts';
import { handleDoctorCommand } from '../session-doctor.ts';
import { createHostDiagnostics } from '../../../platform-runtime-host-diagnostics.ts';
import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts';
@@ -28,7 +28,7 @@ vi.mock('../session-doctor-app.ts', () => ({
vi.mock('../session-doctor-metro.ts', () => ({
probeMetro: vi.fn(async () => ({ id: 'metro', status: 'pass', summary: 'mocked' })),
}));
vi.mock('../../../provider-device-runtime.ts', () => ({
vi.mock('../../provider-device-admission.ts', () => ({
isActiveProviderDevice: vi.fn(() => false),
}));
@@ -2,7 +2,15 @@ import { test, expect, vi, beforeEach } from 'vitest';
import * as os from 'node:os';
import * as path from 'node:path';
import { LeaseRegistry } from '../../lease-registry.ts';
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
import {
isActiveProviderDevice,
setActiveProviderDeviceRuntimes,
} from '../../../provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../../provider-device-admission.ts';
// The daemon reads provider ownership through its own typed admission seam; production
// installs it from root composition, and these tests compose it the same way.
installProviderDeviceAdmission({ isActive: isActiveProviderDevice });
import { createTestDeviceInventoryGateways } from '../../../__tests__/test-utils/device-inventory-gateways.ts';
import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts';
import { makeSession } from '../../../__tests__/test-utils/session-factories.ts';
@@ -6,7 +6,15 @@ import {
resetGetRuntimeFixture,
} from '../../__tests__/interaction-get-runtime-fixture.ts';
import { captureSnapshot } from '../../snapshot-capture.ts';
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
import {
isActiveProviderDevice,
setActiveProviderDeviceRuntimes,
} from '../../../provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../../provider-device-admission.ts';
// The daemon reads provider ownership through its own typed admission seam; production
// installs it from root composition, and these tests compose it the same way.
installProviderDeviceAdmission({ isActive: isActiveProviderDevice });
import { buildInteractionSurfaceSignature } from '../../interaction-outcome-policy.ts';
import { buildNodes } from '../../../__tests__/test-utils/snapshot-builders.ts';
import { resetSnapshotRuntimeFixture } from '../../__tests__/snapshot-runtime-fixture.ts';
@@ -4,7 +4,15 @@ import { resetGetRuntimeFixture } from '../../__tests__/interaction-get-runtime-
import fs from 'node:fs';
import { captureSnapshot } from '../../snapshot-capture.ts';
import { SessionStore } from '../../session-store.ts';
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
import {
isActiveProviderDevice,
setActiveProviderDeviceRuntimes,
} from '../../../provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../../provider-device-admission.ts';
// The daemon reads provider ownership through its own typed admission seam; production
// installs it from root composition, and these tests compose it the same way.
installProviderDeviceAdmission({ isActive: isActiveProviderDevice });
import { AppError } from '@agent-device/kernel/errors';
import { buildInteractionSurfaceSignature } from '../../interaction-outcome-policy.ts';
import { buildSnapshotPresentationKey } from '@agent-device/kernel/snapshot';
@@ -1,7 +1,15 @@
import { test, expect, vi, afterEach, beforeEach } from 'vitest';
import { legacyDispatchCapture } from '../../__tests__/legacy-snapshot-capture-fixture.ts';
import { resetGetRuntimeFixture } from '../../__tests__/interaction-get-runtime-fixture.ts';
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
import {
isActiveProviderDevice,
setActiveProviderDeviceRuntimes,
} from '../../../provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../../provider-device-admission.ts';
// The daemon reads provider ownership through its own typed admission seam; production
// installs it from root composition, and these tests compose it the same way.
installProviderDeviceAdmission({ isActive: isActiveProviderDevice });
import type { SessionState } from '../../session-state.ts';
import { buildSnapshotPresentationKey } from '@agent-device/kernel/snapshot';
import {
@@ -2,7 +2,15 @@ import { test, expect, vi, afterEach, beforeEach } from 'vitest';
import { legacyDispatchCapture } from '../../__tests__/legacy-snapshot-capture-fixture.ts';
import { resetGetRuntimeFixture } from '../../__tests__/interaction-get-runtime-fixture.ts';
import { resetSnapshotRuntimeFixture } from '../../__tests__/snapshot-runtime-fixture.ts';
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
import {
isActiveProviderDevice,
setActiveProviderDeviceRuntimes,
} from '../../../provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../../provider-device-admission.ts';
// The daemon reads provider ownership through its own typed admission seam; production
// installs it from root composition, and these tests compose it the same way.
installProviderDeviceAdmission({ isActive: isActiveProviderDevice });
import type { DaemonResponse } from '../../daemon-request.ts';
import type { SessionState } from '../../session-state.ts';
import {
@@ -14,7 +14,15 @@ import { platformResourceCleanup } from '../../../platform-runtime-resource-clea
import { closeIosApp } from '@agent-device/platform-apple/app-lifecycle';
import { stopIosRunnerSession } from '@agent-device/platform-apple/runner/operations';
import { IOS_SIMULATOR } from '../../../__tests__/test-utils/device-fixtures.ts';
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
import {
isActiveProviderDevice,
setActiveProviderDeviceRuntimes,
} from '../../../provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../../provider-device-admission.ts';
// The daemon reads provider ownership through its own typed admission seam; production
// installs it from root composition, and these tests compose it the same way.
installProviderDeviceAdmission({ isActive: isActiveProviderDevice });
import type { ProviderDeviceRuntime } from '@agent-device/contracts/device';
const mockStopIosRunnerSession = vi.mocked(stopIosRunnerSession);
@@ -1,7 +1,15 @@
import { test, expect, vi, afterEach, beforeEach } from 'vitest';
import { legacyDispatchCapture } from '../../__tests__/legacy-snapshot-capture-fixture.ts';
import { handleSnapshotCommands as handleProductionSnapshotCommands } from '../snapshot.ts';
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
import {
isActiveProviderDevice,
setActiveProviderDeviceRuntimes,
} from '../../../provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../../provider-device-admission.ts';
// The daemon reads provider ownership through its own typed admission seam; production
// installs it from root composition, and these tests compose it the same way.
installProviderDeviceAdmission({ isActive: isActiveProviderDevice });
import { platformResourceCleanup } from '../../../platform-runtime-resource-cleanup.ts';
import {
fixtureSettingsMutations,
+1 -1
View File
@@ -3,7 +3,7 @@ import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
import { isIosFamily, publicPlatformString, type DeviceInfo } from '@agent-device/kernel/device';
import { AppError } from '@agent-device/kernel/errors';
import { emitRequestProgress } from '@agent-device/host-kit/request';
import { isActiveProviderDevice } from '../../provider-device-runtime.ts';
import { isActiveProviderDevice } from '../provider-device-admission.ts';
import {
listLocalDeviceInventory,
shouldPropagateDeviceInventoryProbeError,
@@ -24,7 +24,7 @@ import {
type Point,
} from '@agent-device/kernel/snapshot';
import { resolveBoundGestureRuntime, type BoundGestureExecutor } from '../../gesture-runtime.ts';
import { isActiveProviderDevice } from '../../../provider-device-runtime.ts';
import { isActiveProviderDevice } from '../../provider-device-admission.ts';
import { sleep } from '@agent-device/host-kit/retry';
import { ensureAndroidBlockingSystemDialogReady } from '../../android-system-dialog.ts';
import { readRefMutationFrame } from '../../ref-frame.ts';
@@ -6,7 +6,7 @@ import type { SessionStore } from '../../session-store.ts';
import { getSnapshotReferenceFrame } from '../../touch-reference-frame.ts';
import type { SessionState } from '../../session-state.ts';
import type { BoundContextFromFlags, CaptureSnapshotForSession } from './types.ts';
import { isActiveProviderDevice } from '../../../provider-device-runtime.ts';
import { isActiveProviderDevice } from '../../provider-device-admission.ts';
async function resolveDirectTouchReferenceFrame(params: {
session: SessionState;
+38
View File
@@ -0,0 +1,38 @@
import type { DeviceInfo } from '@agent-device/kernel/device';
/**
* Whether a device is currently owned by a provider runtime (a cloud or remote lease holder)
* rather than a local simulator or emulator. Callers branch on it for fast paths, foreground
* reads, and hint policy.
*/
export type ProviderDeviceAdmission = Readonly<{
isActive(device: DeviceInfo): boolean;
}>;
/**
* The no-provider state, which is what an un-composed process sees: every device is local.
* Root composition replaces it through `installProviderDeviceAdmission` before requests run,
* the same way it installs the other request-scoped runtime capabilities.
*/
const NO_PROVIDER_DEVICE_ADMISSION: ProviderDeviceAdmission = {
isActive: () => false,
};
let installedProviderDeviceAdmission: ProviderDeviceAdmission = NO_PROVIDER_DEVICE_ADMISSION;
/**
* Root composition's installation point. The provider runtime scope is ambient per request, so
* this is the one place the daemon names where the fact comes from, and the daemon never reads
* provider runtime internals itself.
*/
export function installProviderDeviceAdmission(admission: ProviderDeviceAdmission): void {
installedProviderDeviceAdmission = admission;
}
export function providerDeviceAdmission(): ProviderDeviceAdmission {
return installedProviderDeviceAdmission;
}
export function isActiveProviderDevice(device: DeviceInfo): boolean {
return installedProviderDeviceAdmission.isActive(device);
}
+1 -1
View File
@@ -20,7 +20,7 @@ import {
resolveRefFrameEffect,
shouldGuardAndroidBlockingDialog,
} from './daemon-command-registry.ts';
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
import { isActiveProviderDevice } from './provider-device-admission.ts';
import { buildActionEventResult } from '@agent-device/session-journal/session-event-action-presentation';
import type { AndroidObservationAdapter } from '@agent-device/contracts/android-observation';
+6 -1
View File
@@ -5,7 +5,11 @@ import { resolveSessionRequestLogPath } from '../session-artifact-paths.ts';
import { resolveDaemonPaths, resolveDaemonServerMode } from '../config.ts';
import { createDaemonHttpServer } from './http-server.ts';
import { trackDownloadableArtifact } from '../artifact-tracking.ts';
import { createProviderDeviceRuntimeRequestProviders } from '../../provider-device-runtime.ts';
import {
createProviderDeviceRuntimeRequestProviders,
isActiveProviderDevice,
} from '../../provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../provider-device-admission.ts';
import {
androidObservation,
createPlatformRuntimeGateway,
@@ -277,6 +281,7 @@ export async function startDaemonRuntime(
providerDeviceRuntimes,
{ providerRuntimeRequiredIds: DEFAULT_PROVIDER_RUNTIME_REQUIRED_IDS },
);
installProviderDeviceAdmission({ isActive: (device) => isActiveProviderDevice(device) });
const requestPlatformProviders = createRequestPlatformProviders({
providers: {
appleRunnerProvider: providerRuntimeProviders.appleRunnerProvider,
+1 -1
View File
@@ -1,6 +1,6 @@
import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device';
import { AppError } from '@agent-device/kernel/errors';
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
import { isActiveProviderDevice } from './provider-device-admission.ts';
import { appleSessionObservation } from '../platform-runtime-apple-resources.ts';
import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve';
import type { DaemonRequest, DaemonResponse } from './daemon-request.ts';
+1 -1
View File
@@ -2,7 +2,7 @@ import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-res
import type { PlatformResourceCleanup } from './platform-resource-cleanup.ts';
import type { DaemonRequest } from './daemon-request.ts';
import type { SessionScope, SessionState } from './session-state.ts';
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
import { isActiveProviderDevice } from './provider-device-admission.ts';
import { SessionStore } from './session-store.ts';
export async function resolveSessionDevice(
@@ -42,10 +42,16 @@ import {
import { createHostDiagnostics } from '../../../src/platform-runtime-host-diagnostics.ts';
import type { PlatformRuntimeProviderRegistration } from '../../../src/platform-runtime-gateway.ts';
import { createProviderPlatformRuntimeRegistrations } from '../../../src/provider-device-runtimes.ts';
import { isActiveProviderDevice } from '../../../src/provider-device-runtime.ts';
import { installProviderDeviceAdmission } from '../../../src/daemon/provider-device-admission.ts';
import { unavailableDeviceRuntimeGateway } from '../../../src/daemon/__tests__/test-device-runtime-gateway.ts';
import { openWebSessionNames } from '../../../src/daemon/web-session-names.ts';
// Match daemon composition (src/daemon/server/daemon-runtime.ts): the daemon decides on provider
// ownership through its own admission seam, which root composition installs.
installProviderDeviceAdmission({ isActive: (device) => isActiveProviderDevice(device) });
const PROVIDER_SCENARIO_TOKEN = 'provider-scenario-token';
const PROVIDER_SCENARIO_TEMP_REMOVE_OPTIONS = {
recursive: true,