diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b3ae3024b..281a38e7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,10 @@ are unchanged at schema v2. Two notes for mixed installations: a daemon older than this release reads a v3 file as an unreadable claim record and fails closed rather than clearing it, and `devices` reports no `claimedBy` for such a device until the managed-inventory filter lands. +- 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/ad-script/src/internal/__tests__/script-utils.test.ts b/packages/ad-script/src/internal/__tests__/script-utils.test.ts new file mode 100644 index 0000000000..ebb9fd1901 --- /dev/null +++ b/packages/ad-script/src/internal/__tests__/script-utils.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from 'vitest'; +import type { SessionAction } from '@agent-device/contracts/session'; +import { formatPortableActionLine } from '../script-formatting.ts'; +import { parseReplayScriptDetailed } from '../script.ts'; + +test.each(['ios', 'android', 'harmonyos'] as const)( + '%s runtime survives open and runtime set script roundtrips', + (platform) => { + const runtime = { platform, metroHost: 'localhost', metroPort: 8081 }; + const actions: SessionAction[] = [ + { ts: 0, command: 'runtime', positionals: ['set'], flags: runtime }, + { ts: 1, command: 'open', positionals: ['Demo'], flags: {}, runtime }, + ]; + const script = actions.map((action) => formatPortableActionLine(action)).join('\n'); + const parsed = parseReplayScriptDetailed(script); + expect(parsed.actions[0]?.flags).toMatchObject(runtime); + expect(parsed.actions[1]?.runtime).toMatchObject(runtime); + }, +); diff --git a/packages/ad-script/src/internal/script-utils.ts b/packages/ad-script/src/internal/script-utils.ts index d9566b1328..b8491fa3aa 100644 --- a/packages/ad-script/src/internal/script-utils.ts +++ b/packages/ad-script/src/internal/script-utils.ts @@ -1,7 +1,20 @@ import type { SessionAction } from '@agent-device/contracts/session'; +import type { SessionRuntimeHints } from '@agent-device/kernel/contracts'; import { appendScreenshotScriptFlags } from '@agent-device/contracts/capture'; import { splitRefGenerationSuffix } from '@agent-device/kernel/snapshot'; +const SCRIPT_RUNTIME_PLATFORMS = { + ios: true, + android: true, + harmonyos: true, +} satisfies Record, true>; + +function isScriptRuntimePlatform( + value: unknown, +): value is NonNullable { + return typeof value === 'string' && Object.hasOwn(SCRIPT_RUNTIME_PLATFORMS, value); +} + /** * #1076 versioned refs: a recorded ref positional may carry a `~s` * pin from the client that issued it (`@e12~s3`). Generations are meaningless @@ -164,19 +177,10 @@ export function appendScriptSeriesFlags( export function appendRuntimeHintFlags( parts: string[], - flags: - | Pick['flags'] - | { - platform?: 'ios' | 'android'; - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - launchUrl?: string; - } - | undefined, + flags: Pick['flags'] | SessionRuntimeHints | undefined, ): void { if (!flags) return; - if (flags.platform === 'ios' || flags.platform === 'android') { + if (isScriptRuntimePlatform(flags.platform)) { parts.push('--platform', flags.platform); } if (typeof flags.metroHost === 'string' && flags.metroHost.length > 0) { @@ -323,29 +327,17 @@ export function parseReplaySeriesFlags( // fallow-ignore-next-line complexity export function parseReplayRuntimeFlags(args: string[]): { positionals: string[]; - flags: { - platform?: 'ios' | 'android'; - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - launchUrl?: string; - }; + flags: SessionRuntimeHints; } { const positionals: string[] = []; - const flags: { - platform?: 'ios' | 'android'; - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - launchUrl?: string; - } = {}; + const flags: SessionRuntimeHints = {}; for (let index = 0; index < args.length; index += 1) { const token = args[index]!; const nextArg = args[index + 1]; if (token === '--platform' && nextArg !== undefined) { const platform = nextArg; - if (platform === 'ios' || platform === 'android') { + if (isScriptRuntimePlatform(platform)) { flags.platform = platform; } index += 1; diff --git a/packages/kernel/src/contracts.ts b/packages/kernel/src/contracts.ts index 7581203438..104a5c1f5d 100644 --- a/packages/kernel/src/contracts.ts +++ b/packages/kernel/src/contracts.ts @@ -4,8 +4,15 @@ export type { AppErrorCode } from './errors.ts'; export { defaultHintForCode, normalizeError } from './errors.ts'; import type { PlatformSelector } from './device.ts'; +const SESSION_RUNTIME_PLATFORMS = ['ios', 'android', 'harmonyos'] as const; +export type SessionRuntimePlatform = (typeof SESSION_RUNTIME_PLATFORMS)[number]; + +export function isSessionRuntimePlatform(value: unknown): value is SessionRuntimePlatform { + return SESSION_RUNTIME_PLATFORMS.some((platform) => platform === value); +} + export type SessionRuntimeHints = { - platform?: 'ios' | 'android'; + platform?: SessionRuntimePlatform; metroHost?: string; metroPort?: number; bundleUrl?: string; @@ -44,7 +51,12 @@ 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', SESSION_RUNTIME_PLATFORMS, path), metroHost: optionalString(record, 'metroHost', path), metroPort: optionalInteger(record, 'metroPort', path), bundleUrl: optionalString(record, 'bundleUrl', path), diff --git a/src/__tests__/remote-connection-harmonyos.test.ts b/src/__tests__/remote-connection-harmonyos.test.ts new file mode 100644 index 0000000000..f35b4a0923 --- /dev/null +++ b/src/__tests__/remote-connection-harmonyos.test.ts @@ -0,0 +1,145 @@ +import { expect, test } from 'vitest'; +import fs from 'node:fs'; +import { + connectionWorkspace, + createTestClient, + forceConnectFlags, + seedConnectionState, +} from './remote-connection.fixtures.ts'; +import { + materializeRemoteConnectionForCommand, + resolveRequestedLeaseBackend, +} from '../cli/commands/connection-runtime.ts'; +import { disconnectCommand } from '../cli/commands/connection.ts'; +import { readRemoteConnectionState } from '../remote/remote-connection-state.ts'; +import { LeaseRegistry } from '../daemon/lease-registry.ts'; + +test('HarmonyOS platform resolves to its proxy lease backend', () => { + expect( + resolveRequestedLeaseBackend( + forceConnectFlags({ + stateDir: '/tmp/agent-device', + remoteConfig: '/tmp/remote.json', + platform: 'harmonyos', + }), + ), + ).toBe('harmonyos-instance'); +}); + +test.each(['apple', 'harmonyos', 'ios', 'android'] as const)( + 'stored Harmony runtime compatibility respects %s selection', + async (platform) => { + const { stateDir, remoteConfigPath } = connectionWorkspace('harmonyos-runtime-'); + fs.writeFileSync(remoteConfigPath, JSON.stringify({ daemonBaseUrl: 'https://daemon.example' })); + seedConnectionState({ + stateDir, + state: { + session: 'runtime-compat', + remoteConfigPath, + daemon: { baseUrl: 'https://daemon.example' }, + tenant: 'proxy', + runId: 'compat-run', + leaseId: 'compat-existing', + leaseBackend: 'harmonyos-instance', + runtime: { platform: 'harmonyos', launchUrl: 'demo://open' }, + }, + }); + const materialized = await materializeRemoteConnectionForCommand({ + command: 'snapshot', + client: createTestClient(), + flags: { + json: true, + help: false, + version: false, + stateDir, + remoteConfig: remoteConfigPath, + session: 'runtime-compat', + platform, + }, + }); + if (platform === 'apple' || platform === 'harmonyos') { + expect(materialized.runtime?.platform).toBe('harmonyos'); + } else { + expect(materialized.runtime).toBeUndefined(); + } + }, +); + +test('proxy HarmonyOS inventory materializes a scoped lease and closes that same lease', async () => { + const { stateDir, remoteConfigPath } = connectionWorkspace('harmonyos-proxy-'); + fs.writeFileSync(remoteConfigPath, JSON.stringify({ daemonBaseUrl: 'https://daemon.example' })); + seedConnectionState({ + stateDir, + state: { + session: 'harmony-proxy', + remoteConfigPath, + daemon: { baseUrl: 'https://daemon.example' }, + tenant: 'proxy', + runId: 'harmony-run', + leaseProvider: 'proxy', + clientId: 'harmony-client', + }, + }); + const registry = new LeaseRegistry(); + const allocated: string[] = []; + const released: string[] = []; + const client = createTestClient({ + listDevices: async () => [ + { + platform: 'harmonyos', + target: 'mobile', + kind: 'emulator', + id: '127.0.0.1:16001', + name: 'Harmony Emulator', + booted: true, + identifiers: { serial: '127.0.0.1:16001' }, + harmonyos: { serial: '127.0.0.1:16001' }, + }, + ], + allocate: async (request) => { + expect(request.leaseBackend).toBe('harmonyos-instance'); + expect(request.deviceKey).toBe('harmonyos:mobile:127.0.0.1:16001'); + const lease = registry.allocateLease({ ...request, tenantId: request.tenant }); + allocated.push(lease.leaseId); + return lease; + }, + heartbeat: async (request) => registry.heartbeatLease({ ...request, tenantId: request.tenant }), + release: async (request) => { + const result = registry.releaseLease({ ...request, tenantId: request.tenant }); + expect(result.released).toBe(true); + released.push(request.leaseId); + return result; + }, + }); + const flags = { + json: true, + help: false, + version: false, + stateDir, + remoteConfig: remoteConfigPath, + session: 'harmony-proxy', + }; + const materialized = await materializeRemoteConnectionForCommand({ + command: 'open', + flags, + client, + }); + expect(materialized.flags.platform).toBe('harmonyos'); + expect(materialized.flags.serial).toBe('127.0.0.1:16001'); + expect(materialized.flags.leaseBackend).toBe('harmonyos-instance'); + expect(materialized.flags.leaseId).toBe(allocated[0]); + expect(readRemoteConnectionState({ stateDir, session: flags.session })?.leaseId).toBe( + allocated[0], + ); + expect(() => + registry.assertLeaseAdmission({ + leaseId: allocated[0], + tenantId: 'proxy', + runId: 'harmony-run', + leaseBackend: 'android-instance', + }), + ).toThrow(); + await disconnectCommand({ positionals: [], flags, client }); + expect(released).toEqual(allocated); + expect(readRemoteConnectionState({ stateDir, session: flags.session })).toBeNull(); +}); diff --git a/src/cli/commands/connection-runtime.ts b/src/cli/commands/connection-runtime.ts index 59b620611f..987f218823 100644 --- a/src/cli/commands/connection-runtime.ts +++ b/src/cli/commands/connection-runtime.ts @@ -28,7 +28,11 @@ import { import { profileToCliFlags } from '../remote-config-flags.ts'; import type { BatchStep } from '@agent-device/contracts/client'; import { AppError } from '@agent-device/kernel/errors'; -import type { LeaseBackend, SessionRuntimeHints } from '@agent-device/kernel/contracts'; +import { + isSessionRuntimePlatform, + type LeaseBackend, + type SessionRuntimeHints, +} from '@agent-device/kernel/contracts'; import type { CliFlags } from '@agent-device/contracts/command'; import type { AgentDeviceClient, Lease } from '../../agent-device-client.ts'; import type { CloudProviderSessionResult } from '@agent-device/contracts/observability'; @@ -689,6 +693,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 +702,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 +738,7 @@ function isRuntimeCompatibleWithPlatform( runtime: SessionRuntimeHints, platform: CliFlags['platform'], ): boolean { - if (!runtime.platform || !platform || (platform !== 'ios' && platform !== 'android')) { + if (!runtime.platform || !platform || !isSessionRuntimePlatform(platform)) { return true; } return runtime.platform === platform; @@ -887,7 +892,7 @@ function applyResolvedDeviceSelector(flags: CliFlags, device: DeviceInfo): void flags.udid = device.id; return; } - if (device.platform === 'android') { + if (device.platform === 'android' || device.platform === 'harmonyos') { flags.serial = device.id; } } @@ -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/client/client-normalizers.test.ts b/src/client/client-normalizers.test.ts index 43fc56511e..09b286295e 100644 --- a/src/client/client-normalizers.test.ts +++ b/src/client/client-normalizers.test.ts @@ -1,6 +1,24 @@ import assert from 'node:assert/strict'; import { expect, test } from 'vitest'; -import { normalizeDevice, normalizeOpenForegroundComposition } from './client-normalizers.ts'; +import { + normalizeDevice, + normalizeOpenForegroundComposition, + normalizeRuntimeHints, +} from './client-normalizers.ts'; + +test.each(['ios', 'android', 'harmonyos'])('runtime response preserves %s platform', (platform) => { + expect(normalizeRuntimeHints({ platform, launchUrl: 'demo://open' })).toMatchObject({ + platform, + launchUrl: 'demo://open', + }); +}); + +test.each(['apple', 'unknown', 12])( + 'runtime response ignores non-runtime platform %s', + (platform) => { + expect(normalizeRuntimeHints({ platform })?.platform).toBeUndefined(); + }, +); test('embedded daemon errors sanitize an untrusted cause before client exposure', () => { const secret = 'adc_live_remote-secret'; diff --git a/src/client/client-normalizers.ts b/src/client/client-normalizers.ts index 7dd9056a2f..d7df742208 100644 --- a/src/client/client-normalizers.ts +++ b/src/client/client-normalizers.ts @@ -17,7 +17,7 @@ import { isSerialAddressablePlatform, type AppleOS, } from '@agent-device/kernel/device'; -import type { SessionRuntimeHints } from '@agent-device/kernel/contracts'; +import { isSessionRuntimePlatform, type SessionRuntimeHints } from '@agent-device/kernel/contracts'; import { AppError, type DaemonError } from '@agent-device/kernel/errors'; import { sanitizeErrorCause } from '@agent-device/kernel/redaction'; import type { SnapshotNode } from '@agent-device/kernel/snapshot'; @@ -200,7 +200,7 @@ export function normalizeRuntimeHints(value: unknown): SessionRuntimeHints | und const bundleUrl = readOptionalString(value, 'bundleUrl'); const launchUrl = readOptionalString(value, 'launchUrl'); return { - platform: platform === 'ios' || platform === 'android' ? platform : undefined, + platform: isSessionRuntimePlatform(platform) ? platform : undefined, metroHost, metroPort, bundleUrl, diff --git a/src/commands/cli-grammar/flag-definitions-connection.ts b/src/commands/cli-grammar/flag-definitions-connection.ts index c59893b4f3..d2ab5cfd66 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 1bb76af552..e89f0fc417 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/__tests__/session-runtime.test.ts b/src/daemon/__tests__/session-runtime.test.ts new file mode 100644 index 0000000000..2435bf51bd --- /dev/null +++ b/src/daemon/__tests__/session-runtime.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from 'vitest'; +import { daemonRuntimeSchema } from '@agent-device/kernel/contracts'; +import { resolveEffectiveOpenRuntimeHints, toRuntimePlatform } from '../session-runtime.ts'; + +test.each(['ios', 'android', 'harmonyos'] as const)( + '%s runtime survives wire admission and open normalization', + (platform) => { + const runtime = daemonRuntimeSchema.parse({ platform, launchUrl: 'demo://open' }); + expect(toRuntimePlatform(platform)).toBe(platform); + const normalized = resolveEffectiveOpenRuntimeHints({ + req: { token: 'test', session: 'runtime-test', command: 'open', positionals: [], runtime }, + sessionName: 'runtime-test', + platform, + sessionStore: { getRuntimeHints: () => undefined }, + }); + expect(normalized).toMatchObject(runtime); + expect(() => + resolveEffectiveOpenRuntimeHints({ + req: { token: 'test', session: 'runtime-test', command: 'open', positionals: [], runtime }, + sessionName: 'runtime-test', + platform: platform === 'ios' ? 'android' : 'ios', + sessionStore: { getRuntimeHints: () => undefined }, + }), + ).toThrow(); + }, +); + +test('non-leaf apple selector is not a runtime platform', () => { + expect(toRuntimePlatform('apple')).toBeUndefined(); + expect(() => daemonRuntimeSchema.parse({ platform: 'apple' })).toThrow(); +}); diff --git a/src/daemon/handlers/session-runtime-command.ts b/src/daemon/handlers/session-runtime-command.ts index 4d1025f9be..d68e2c9ef1 100644 --- a/src/daemon/handlers/session-runtime-command.ts +++ b/src/daemon/handlers/session-runtime-command.ts @@ -178,7 +178,7 @@ function setRuntimeCommand(params: { if (!platform) { return errorResponse( 'INVALID_ARGS', - 'runtime set only supports iOS and Android sessions. Pass --platform ios|android or open an iOS/Android session first.', + 'runtime set only supports iOS, Android, and HarmonyOS sessions. Pass --platform ios|android|harmonyos or open a supported session first.', ); } if (sessionLeaf !== undefined && sessionLeaf !== platform) { diff --git a/src/daemon/lease-registry-scope.ts b/src/daemon/lease-registry-scope.ts index 614afd2449..290bf86729 100644 --- a/src/daemon/lease-registry-scope.ts +++ b/src/daemon/lease-registry-scope.ts @@ -133,7 +133,8 @@ 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 4387a934e5..2dea4fd944 100644 --- a/src/daemon/session-runtime.ts +++ b/src/daemon/session-runtime.ts @@ -4,6 +4,7 @@ import { hasRuntimeTransportHintValues, } from '@agent-device/contracts/application-lifecycle-runtime'; import { AppError, asAppError } from '@agent-device/kernel/errors'; +import { isSessionRuntimePlatform } from '@agent-device/kernel/contracts'; import { publicPlatformString, type DeviceInfo } from '@agent-device/kernel/device'; import type { DaemonRequest } from './daemon-request.ts'; import type { SessionRuntimeHints, SessionState } from './session-state.ts'; @@ -87,10 +88,10 @@ function normalizeRuntimePlatformInput( platform?: RuntimePlatform, ): RuntimePlatform | undefined { if (value === undefined) return platform; - if (value !== 'ios' && value !== 'android') { + if (!isSessionRuntimePlatform(value)) { 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) { @@ -105,7 +106,7 @@ function normalizeRuntimePlatformInput( export function toRuntimePlatform( platform: CommandFlags['platform'] | DeviceInfo['platform'] | 'apple' | undefined, ): RuntimePlatform | undefined { - if (platform === 'ios' || platform === 'android') { + if (isSessionRuntimePlatform(platform)) { return platform; } return undefined; @@ -216,7 +217,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 6c33f206dd..f3cfc27527 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 63f0071c03..9ace1b3b7c 100644 --- a/test/wire-compat/ledger.json +++ b/test/wire-compat/ledger.json @@ -26,16 +26,18 @@ "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:1b29207af3c9ec65b999e4d3f9ab839004472897f748f8d4f4ab964de54efc9f", "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#SESSION_RUNTIME_PLATFORMS": "sha256:6164fc8ca226b5beb3ba52a8dddd3c11633a8accec58b9f8972c44c8339a1bf7", + "packages/kernel/src/contracts.ts#SessionRuntimePlatform": "sha256:694b301564f2c9da5d252be7e26c20bd53eed715b27033c2162c2a11606a9092", + "packages/kernel/src/contracts.ts#SessionRuntimeHints": "sha256:94b4d9778b6bd3c5fb401fab6fe253473e4950f477e2d7c2fc02630ef7fdbb62", "packages/kernel/src/contracts.ts#commandRpcParamsSchema": "sha256:d0ce05c236ce5b0b52631ab7b8a75d38859fbfba58cfb28dd3f9d84f2dba2c1f", - "packages/kernel/src/contracts.ts#daemonRuntimeSchema": "sha256:010c171164ce9516e1c1b00e89e698a11842ff9c3bac3f2e8e3814de7c8be78c", + "packages/kernel/src/contracts.ts#daemonRuntimeSchema": "sha256:3b99926ce9deb5d66186a55b1168011dc1ae4d010ada5ac5d79c4c7225a1f80f", "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", @@ -283,6 +285,21 @@ "declaration": "src/daemon/request-diagnostics-http.ts#handleRequestDiagnostics", "digest": "sha256:32c3d100176dd99ecbb61f8050d05b1cda64b7be1944a2cd16633e83fa78660b", "rationale": "#2198 context: asks isTenantAddressableSessionName instead of applying the prefix rule locally. Same UNAUTHORIZED code, same 'Session is outside this tenant' message and same 401 body for the attested-tenant case the rule still refuses; the only difference a peer can observe is that an unattested tenant now gets the 200 + ndjson record it already knows how to read instead of a 401 it could not act on." + }, + { + "declaration": "packages/kernel/src/contracts.ts#LEASE_BACKENDS", + "digest": "sha256:1b29207af3c9ec65b999e4d3f9ab839004472897f748f8d4f4ab964de54efc9f", + "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:94b4d9778b6bd3c5fb401fab6fe253473e4950f477e2d7c2fc02630ef7fdbb62", + "rationale": "#2266 derives the optional runtime platform from one shared vocabulary, adding harmonyos without changing existing iOS/Android payloads. Older peers can refuse the new value with the existing validation error." + }, + { + "declaration": "packages/kernel/src/contracts.ts#daemonRuntimeSchema", + "digest": "sha256:3b99926ce9deb5d66186a55b1168011dc1ae4d010ada5ac5d79c4c7225a1f80f", + "rationale": "#2266 broadens runtime-hint validation to accept the additive HarmonyOS platform value; existing protocol-2 runtime hints remain valid and unchanged." } ] } diff --git a/test/wire-compat/surface.ts b/test/wire-compat/surface.ts index 1fcf84081f..3043bf34ec 100644 --- a/test/wire-compat/surface.ts +++ b/test/wire-compat/surface.ts @@ -177,6 +177,8 @@ export const WIRE_SURFACE: readonly WireSurfaceGroup[] = [ 'DaemonRequest', 'DaemonRequestMeta', 'SessionRuntimeHints', + 'SESSION_RUNTIME_PLATFORMS', + 'SessionRuntimePlatform', 'daemonRuntimeSchema', 'DaemonInstallSource', 'DAEMON_LOCK_POLICIES',