diff --git a/CHANGELOG.md b/CHANGELOG.md index eb125d153..42e8e687f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ - Added strict `wait absent [timeoutMs]` polling for zero selector matches. Incomplete, sparse, truncated, scoped, depth-limited, and Android unreadable captures cannot prove absence; deadline diagnostics retain typed capture evidence and stable first-match details (#2236). +- Added the `harmonyos-instance` lease contract and CLI/runtime plumbing as a prerequisite for + HarmonyOS proxy support; provider/daemon allocation remains gated until its end-to-end lifecycle + is implemented and validated (#2266). + - Fixed: `settings airplane on|off` now takes an Android device offline. It is applied through the connectivity service (`cmd connectivity airplane-mode`), which drives the radios, instead of writing `airplane_mode_on` and broadcasting `ACTION_AIRPLANE_MODE_CHANGED` — a broadcast Android diff --git a/packages/kernel/src/contracts.ts b/packages/kernel/src/contracts.ts index a42a4154b..a42ee3462 100644 --- a/packages/kernel/src/contracts.ts +++ b/packages/kernel/src/contracts.ts @@ -5,7 +5,7 @@ export { defaultHintForCode, normalizeError } from './errors.ts'; import type { PlatformSelector } from './device.ts'; export type SessionRuntimeHints = { - platform?: 'ios' | 'android'; + platform?: 'ios' | 'android' | 'harmonyos'; metroHost?: string; metroPort?: number; bundleUrl?: string; @@ -44,7 +44,7 @@ export type LocalInstallSource = Extract( export const daemonRuntimeSchema = schema((input, path) => { const record = expectObject(input, path); return { - platform: optionalEnum(record, 'platform', ['ios', 'android'] as const, path), + platform: optionalEnum(record, 'platform', ['ios', 'android', 'harmonyos'] as const, path), metroHost: optionalString(record, 'metroHost', path), metroPort: optionalInteger(record, 'metroPort', path), bundleUrl: optionalString(record, 'bundleUrl', path), diff --git a/src/__tests__/remote-connection.test.ts b/src/__tests__/remote-connection.test.ts index c97a819e4..216999ddf 100644 --- a/src/__tests__/remote-connection.test.ts +++ b/src/__tests__/remote-connection.test.ts @@ -34,7 +34,9 @@ import { materializeRemoteConnectionForCommand, CLOUD_WEBDRIVER_REMOTE_LEASE_TTL_MS, PROXY_REMOTE_LEASE_TTL_MS, + resolveRequestedLeaseBackend, } from '../cli/commands/connection-runtime.ts'; + import { stopMetroCompanion } from '../metro/client-metro-companion.ts'; import { AppError } from '@agent-device/kernel/errors'; import { @@ -45,6 +47,15 @@ import { } from '../remote/remote-connection-state.ts'; import type { AgentDeviceClient } from '../agent-device-client.ts'; +test('HarmonyOS platform resolves to its proxy lease backend', () => { + assert.equal( + resolveRequestedLeaseBackend( + forceConnectFlags({ stateDir: '/tmp/agent-device', remoteConfig: '/tmp/remote.json', platform: 'harmonyos' }), + ), + 'harmonyos-instance', + ); +}); + afterEach(() => { vi.clearAllMocks(); vi.restoreAllMocks(); diff --git a/src/cli/commands/connection-runtime.ts b/src/cli/commands/connection-runtime.ts index eb6cc6e81..871aabd06 100644 --- a/src/cli/commands/connection-runtime.ts +++ b/src/cli/commands/connection-runtime.ts @@ -689,6 +689,7 @@ export function resolveRequestedLeaseBackend(flags: CliFlags): LeaseBackend | un if (flags.leaseBackend) return flags.leaseBackend; if (flags.platform === 'android') return 'android-instance'; if (flags.platform === 'ios') return 'ios-instance'; + if (flags.platform === 'harmonyos') return 'harmonyos-instance'; return undefined; } @@ -697,7 +698,7 @@ function requireRequestedLeaseBackend(flags: CliFlags, command: string): LeaseBa if (leaseBackend) return leaseBackend; throw new AppError( 'INVALID_ARGS', - `${command} requires --platform ios|android or --lease-backend when the remote connection has not resolved a lease yet.`, + `${command} requires --platform ios|android|harmonyos or --lease-backend when the remote connection has not resolved a lease yet.`, ); } @@ -733,7 +734,11 @@ function isRuntimeCompatibleWithPlatform( runtime: SessionRuntimeHints, platform: CliFlags['platform'], ): boolean { - if (!runtime.platform || !platform || (platform !== 'ios' && platform !== 'android')) { + if ( + !runtime.platform || + !platform || + (platform !== 'ios' && platform !== 'android' && platform !== 'harmonyos') + ) { return true; } return runtime.platform === platform; @@ -931,6 +936,7 @@ function buildProxyDeviceKey(device: DeviceInfo): string { function leaseBackendForDevice(device: DeviceInfo): LeaseBackend | undefined { if (isIosFamily(device)) return 'ios-instance'; if (device.platform === 'android') return 'android-instance'; + if (device.platform === 'harmonyos') return 'harmonyos-instance'; return undefined; } diff --git a/src/commands/cli-grammar/flag-definitions-connection.ts b/src/commands/cli-grammar/flag-definitions-connection.ts index c59893b4f..d2ab5cfd6 100644 --- a/src/commands/cli-grammar/flag-definitions-connection.ts +++ b/src/commands/cli-grammar/flag-definitions-connection.ts @@ -105,8 +105,8 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ key: 'leaseBackend', names: ['--lease-backend'], type: 'enum', - enumValues: ['ios-simulator', 'ios-instance', 'android-instance'], - usageLabel: '--lease-backend ios-simulator|ios-instance|android-instance', + enumValues: ['ios-simulator', 'ios-instance', 'android-instance', 'harmonyos-instance'], + usageLabel: '--lease-backend ios-simulator|ios-instance|android-instance|harmonyos-instance', usageDescription: 'Lease backend for remote tenant connection admission', }, { diff --git a/src/daemon/__tests__/lease-registry-scope.test.ts b/src/daemon/__tests__/lease-registry-scope.test.ts index 1bb76af55..e89f0fc41 100644 --- a/src/daemon/__tests__/lease-registry-scope.test.ts +++ b/src/daemon/__tests__/lease-registry-scope.test.ts @@ -4,9 +4,14 @@ import { createLeaseTtlResolver, leaseDeviceBindingKey, normalizeAllocateLeaseRequest, + normalizeLeaseBackend, } from '../lease-registry-scope.ts'; import { HUMAN_CONTROL_LEASE_REQUEST, HUMAN_CONTROL_SCOPE } from './human-control-fixtures.ts'; +test('normalizeLeaseBackend accepts HarmonyOS instance backend', () => { + assert.equal(normalizeLeaseBackend('harmonyos-instance'), 'harmonyos-instance'); +}); + test('allocation and human control share the exact contention identity', () => { const scope = normalizeAllocateLeaseRequest(HUMAN_CONTROL_LEASE_REQUEST); assert.equal(leaseDeviceBindingKey(scope), leaseDeviceBindingKey(HUMAN_CONTROL_SCOPE)); diff --git a/src/daemon/lease-registry-scope.ts b/src/daemon/lease-registry-scope.ts index 614afd244..f3d6ca59e 100644 --- a/src/daemon/lease-registry-scope.ts +++ b/src/daemon/lease-registry-scope.ts @@ -133,7 +133,7 @@ export function normalizeRequiredLeaseId(raw: string | undefined): string { export function normalizeLeaseBackend(raw: string | undefined): LeaseBackend { const value = (raw ?? '').trim().toLowerCase(); if (!value || value === 'ios-simulator') return 'ios-simulator'; - if (value === 'ios-instance' || value === 'android-instance') return value; + if (value === 'ios-instance' || value === 'android-instance' || value === 'harmonyos-instance') return value; throw new AppError('INVALID_ARGS', `Unsupported lease backend: ${raw ?? ''}`); } diff --git a/src/daemon/session-runtime.ts b/src/daemon/session-runtime.ts index 52b68b84a..aaaf2dbf5 100644 --- a/src/daemon/session-runtime.ts +++ b/src/daemon/session-runtime.ts @@ -86,10 +86,10 @@ function normalizeRuntimePlatformInput( platform?: RuntimePlatform, ): RuntimePlatform | undefined { if (value === undefined) return platform; - if (value !== 'ios' && value !== 'android') { + if (value !== 'ios' && value !== 'android' && value !== 'harmonyos') { throw new AppError( 'INVALID_ARGS', - `Invalid open runtime platform: ${String(value)}. Use "ios" or "android".`, + `Invalid open runtime platform: ${String(value)}. Use "ios", "android", or "harmonyos".`, ); } if (platform && value !== platform) { @@ -104,7 +104,7 @@ function normalizeRuntimePlatformInput( export function toRuntimePlatform( platform: CommandFlags['platform'] | DeviceInfo['platform'] | 'apple' | undefined, ): RuntimePlatform | undefined { - if (platform === 'ios' || platform === 'android') { + if (platform === 'ios' || platform === 'android' || platform === 'harmonyos') { return platform; } return undefined; @@ -215,7 +215,7 @@ function resolveSessionRuntimeHints( if (runtime.platform && device && !deviceRuntimePlatform) { throw new AppError( 'INVALID_ARGS', - `Session runtime hints are only supported on iOS and Android sessions, but session "${sessionName}" is bound to ${boundPlatform}.`, + `Session runtime hints are only supported on iOS, Android, and HarmonyOS sessions, but session "${sessionName}" is bound to ${boundPlatform}.`, ); } if (runtime.platform && deviceRuntimePlatform && runtime.platform !== deviceRuntimePlatform) { diff --git a/src/remote/remote-config-schema.ts b/src/remote/remote-config-schema.ts index 6c33f206d..f3cfc2752 100644 --- a/src/remote/remote-config-schema.ts +++ b/src/remote/remote-config-schema.ts @@ -61,7 +61,7 @@ export const REMOTE_CONFIG_FIELD_SPECS = [ { key: 'leaseBackend', type: 'enum', - enumValues: ['ios-simulator', 'ios-instance', 'android-instance'], + enumValues: ['ios-simulator', 'ios-instance', 'android-instance', 'harmonyos-instance'], }, { key: 'platform', type: 'enum', enumValues: PLATFORM_SELECTORS }, { key: 'target', type: 'enum', enumValues: ['mobile', 'tv', 'desktop'] }, diff --git a/test/wire-compat/ledger.json b/test/wire-compat/ledger.json index 7667d5a13..dc53b63f9 100644 --- a/test/wire-compat/ledger.json +++ b/test/wire-compat/ledger.json @@ -18,16 +18,16 @@ "packages/kernel/src/contracts.ts#DaemonResponseData": "sha256:f16e61b7f0ea8cdae82349508383af2f97a4c16c8aa21952d833094673f86afc", "packages/kernel/src/contracts.ts#JsonRpcId": "sha256:5a5fdff1cd85971214117d69a8278d0ecaf21a6fa3f4582155d9b3d9cad1fbd0", "packages/kernel/src/contracts.ts#JsonRpcRequestEnvelope": "sha256:d6328309fc4f8b310b88680181e46c802329de9b0e45682dd2e0f0c3e8d090e4", - "packages/kernel/src/contracts.ts#LEASE_BACKENDS": "sha256:9fb296dcfd56556e4dd020bcb40f3b70890b274600e634aa2c380d860b38c022", + "packages/kernel/src/contracts.ts#LEASE_BACKENDS": "sha256:6bec996666d1043a8d23e60d5b1e50ef9341909a02f19060a3132bb72dcc3155", "packages/kernel/src/contracts.ts#LeaseBackend": "sha256:dce397343cda82ce4e3e43b534b1f642cd37568ed304f4783caddc3524c1cae5", "packages/kernel/src/contracts.ts#RESPONSE_LEVELS": "sha256:cef29c3b56c299e35a7fee8058b90dc4a6f65a46a420e4f11f77c8ca7d0359c8", "packages/kernel/src/contracts.ts#ResponseCost": "sha256:2a44de54e8b5c886b36da1500e22d634ec7e12961d2c93b689d2b1aa8c15f186", "packages/kernel/src/contracts.ts#ResponseLevel": "sha256:c7329d28e745b5e69b86aa1a686befa314b98248afbe5e0e2d7d395f0d777f9b", "packages/kernel/src/contracts.ts#SESSION_ISOLATION_MODES": "sha256:3d4b3048e22bb0ceccc98cf8fd93a87a1a1e2dad96fa03cffff6d0ecedb58689", "packages/kernel/src/contracts.ts#SessionIsolationMode": "sha256:beea941fe57b000902da28af15a27aa18d0c957408209e3ec372fcf7f605fa7c", - "packages/kernel/src/contracts.ts#SessionRuntimeHints": "sha256:6b945688ad5ab68370ec001d962974e1397c39d47c6f7ca2a65b03f5ccdd91c6", + "packages/kernel/src/contracts.ts#SessionRuntimeHints": "sha256:7e055a99bc6adbb3efc37b0691055f5370f35f8b96ae6af73295ccce61e05dfe", "packages/kernel/src/contracts.ts#commandRpcParamsSchema": "sha256:d0ce05c236ce5b0b52631ab7b8a75d38859fbfba58cfb28dd3f9d84f2dba2c1f", - "packages/kernel/src/contracts.ts#daemonRuntimeSchema": "sha256:010c171164ce9516e1c1b00e89e698a11842ff9c3bac3f2e8e3814de7c8be78c", + "packages/kernel/src/contracts.ts#daemonRuntimeSchema": "sha256:859eabaf6679d117feb2b4873b6ac69731cc06989645080817d39c43a24deea5", "packages/kernel/src/contracts.ts#jsonRpcRequestSchema": "sha256:67e6b8a28b39a3883424a565ae7033c336dc2ea6b193e9b32bb98eb3e18dca7c", "packages/kernel/src/device.ts#PLATFORM_SELECTORS": "sha256:36e9da1cc660c0ddfb4cf52521f7f230341ff20e3ebda3405ba7c1799476c42d", "packages/kernel/src/device.ts#PlatformSelector": "sha256:61de3f003507ea2f53b396b0146c17670bf674a2db2132e19c462ca6c10cc8fd", @@ -227,6 +227,21 @@ "declaration": "src/daemon/client/daemon-client-rpc.ts#appErrorFromDaemonError", "digest": "sha256:ac4761006c71d93ccba9f8e37cbd4cf48283dbb22737c163dc9c1a878e154eea", "rationale": "#1862 client-side only: this rehydrates the new optional structured `cause` when present. Released daemons that omit it follow the unchanged path, and no request field or existing response field is narrowed." + }, + { + "declaration": "packages/kernel/src/contracts.ts#LEASE_BACKENDS", + "digest": "sha256:6bec996666d1043a8d23e60d5b1e50ef9341909a02f19060a3132bb72dcc3155", + "rationale": "#2266 adds the additive harmonyos-instance lease backend literal. Protocol-2 peers ignore the new backend unless explicitly requested; existing iOS/Android values and payloads remain unchanged." + }, + { + "declaration": "packages/kernel/src/contracts.ts#SessionRuntimeHints", + "digest": "sha256:7e055a99bc6adbb3efc37b0691055f5370f35f8b96ae6af73295ccce61e05dfe", + "rationale": "#2266 adds the optional HarmonyOS platform value to runtime hints. Existing iOS/Android hints retain their shape and released peers can ignore the additive value." + }, + { + "declaration": "packages/kernel/src/contracts.ts#daemonRuntimeSchema", + "digest": "sha256:859eabaf6679d117feb2b4873b6ac69731cc06989645080817d39c43a24deea5", + "rationale": "#2266 broadens runtime-hint validation to accept the additive HarmonyOS platform value; existing protocol-2 runtime hints remain valid and unchanged." } ] }