Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/contracts/src/__tests__/lease-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
leaseScopeToConnectionMetadata,
leaseScopeToLeaseRpcParams,
leaseScopeToRequestMeta,
readLeaseAllocateProviderFlags,
} from '../lease-scope.ts';

test('leaseScopeFromOptions normalizes public aliases and projects request meta', () => {
Expand Down Expand Up @@ -166,3 +167,51 @@ test('findMissingProxyLeaseFields enforces complete proxy ownership scope', () =
[],
);
});

test('readLeaseAllocateProviderFlags carries the provider-allocation flags and drops the rest', () => {
assert.deepEqual(
readLeaseAllocateProviderFlags({
session: 'default',
token: 'secret',
runId: 'run-a',
deviceKey: 'dk-1',
provider: 'browserstack',
ttlMs: 1000,
platform: 'ios',
device: 'iPhone 15',
providerApp: 'bs://abc',
providerOsVersion: '17',
providerProject: 'MyProject',
providerBuild: 'Build-1',
providerSessionName: 'smoke',
providerDeviceOrientation: 'landscape',
providerGeoLocation: '52.5,13.4',
providerLanguage: 'en',
providerNoResignApp: true,
awsRegion: 'us-west-2',
awsProjectArn: 'arn:aws:devicefarm:0',
awsInteractionMode: 'NO_VIDEO',
}),
{
platform: 'ios',
device: 'iPhone 15',
providerApp: 'bs://abc',
providerOsVersion: '17',
providerProject: 'MyProject',
providerBuild: 'Build-1',
providerSessionName: 'smoke',
providerDeviceOrientation: 'landscape',
providerGeoLocation: '52.5,13.4',
providerLanguage: 'en',
providerNoResignApp: true,
awsRegion: 'us-west-2',
awsProjectArn: 'arn:aws:devicefarm:0',
awsInteractionMode: 'NO_VIDEO',
},
);
assert.deepEqual(
readLeaseAllocateProviderFlags({ providerApp: 'bs://abc', providerBuild: undefined }),
{ providerApp: 'bs://abc' },
);
assert.deepEqual(readLeaseAllocateProviderFlags(undefined), {});
});
59 changes: 59 additions & 0 deletions packages/contracts/src/lease-scope.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { LeaseBackend } from '@agent-device/kernel/contracts';
import { stripUndefined } from '@agent-device/kernel/record';
import type { CloudProviderProfileFields } from './remote-config-fields.ts';
import type { CommandFlags } from './command-flags.ts';

const PROXY_LEASE_PROVIDER = 'proxy';
export const DEFAULT_PROXY_LEASE_TTL_MS = 300_000;
Expand Down Expand Up @@ -197,6 +199,63 @@ export function leaseScopeToLeaseRpcParams(
}
}

/**
* Request flags that must travel with `lease_allocate` so the daemon's lease-lifecycle provider can
* prepare the session — device selection, the app/os, the session-naming fields, and the configured
* device-feature and AWS knobs. The line transport forwards the whole request and these arrive for
* free; the compact lease envelope is the only projection that has to name them, so the client's
* producer and the daemon's consumer read the SAME list and cannot drop a sibling the provider needs.
*/
const LEASE_ALLOCATE_PROVIDER_FLAG_KEYS = [
// Device selection the lease-lifecycle provider needs to create the session.
'platform',
'device',
// The Cloud provider profile fields; pinned exhaustive against that vocabulary below.
'providerApp',
'providerOsVersion',
'providerProject',
'providerBuild',
'providerSessionName',
'providerDeviceOrientation',
'providerGeoLocation',
'providerTimezone',
'providerLanguage',
'providerLocale',
'providerNetworkProfile',
'providerCustomNetwork',
'providerNoResignApp',
'awsProjectArn',
'awsDeviceArn',
'awsAppArn',
'awsRegion',
'awsInteractionMode',
] as const satisfies readonly (keyof CommandFlags)[];

type LeaseAllocateProviderFlagKey = (typeof LEASE_ALLOCATE_PROVIDER_FLAG_KEYS)[number];

// A Cloud provider profile field added without joining the projection would be dropped from the
// envelope and fail `prepareSession` on the remote daemon — this makes that omission fail to build.
type LeaseAllocateProfileKeysAreExhaustive =
Exclude<keyof CloudProviderProfileFields, LeaseAllocateProviderFlagKey> extends never
? true
: Exclude<keyof CloudProviderProfileFields, LeaseAllocateProviderFlagKey>;
const leaseAllocateProfileKeysExhaustive: LeaseAllocateProfileKeysAreExhaustive = true;
void leaseAllocateProfileKeysExhaustive;

type LeaseAllocateProviderFlags = Partial<Pick<CommandFlags, LeaseAllocateProviderFlagKey>>;

/** Reads the provider-allocation flags out of a request flags bag or a lease-envelope param bag. */
export function readLeaseAllocateProviderFlags(
source: Record<string, unknown> | undefined,
): LeaseAllocateProviderFlags {
const flags: Record<string, string | boolean> = {};
for (const key of LEASE_ALLOCATE_PROVIDER_FLAG_KEYS) {
const value = source?.[key];
if (typeof value === 'string' || typeof value === 'boolean') flags[key] = value;
}
return flags as LeaseAllocateProviderFlags;
}

export function leaseScopeToConnectionMetadata(
scope: LeaseScope,
): Pick<LeaseScope, 'leaseProvider' | 'deviceKey' | 'clientId'> | undefined {
Expand Down
47 changes: 47 additions & 0 deletions src/daemon-client/daemon-client-rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,53 @@ test('lease allocation transports an optional initial provider app', () => {
});
});

test('lease allocation transports the provider configuration the session needs (#2494)', () => {
const payload = buildHttpRpcPayload(
{
token: 'daemon-token',
session: 'qa-ios',
command: 'lease_allocate',
positionals: [],
flags: {
platform: 'ios',
device: 'iPhone 15',
providerApp: 'bs://app-id',
providerOsVersion: '17',
providerProject: 'MyProject',
providerBuild: 'Build-2026-09-11',
providerSessionName: 'smoke — iOS',
providerDeviceOrientation: 'portrait',
providerNoResignApp: true,
},
meta: {
requestId: 'lease-req',
tenantId: 'acme',
runId: 'run-123',
leaseBackend: 'ios-instance',
leaseProvider: 'browserstack',
},
},
{ includeTokenParam: false },
);

assert.deepEqual(payload.params, {
session: 'qa-ios',
tenantId: 'acme',
runId: 'run-123',
backend: 'ios-instance',
leaseProvider: 'browserstack',
platform: 'ios',
device: 'iPhone 15',
providerApp: 'bs://app-id',
providerOsVersion: '17',
providerProject: 'MyProject',
providerBuild: 'Build-2026-09-11',
providerSessionName: 'smoke — iOS',
providerDeviceOrientation: 'portrait',
providerNoResignApp: true,
});
});

test('HTTP RPC errors sanitize an untrusted cause before rehydration', () => {
const secret = 'adc_agent_remote-secret';
const oversizedCode = `apiKey=${secret} ${'c'.repeat(500)}`;
Expand Down
5 changes: 2 additions & 3 deletions src/daemon-client/daemon-client-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { DaemonInfo } from './daemon-client-metadata.ts';
import {
leaseScopeFromRequest,
leaseScopeToLeaseRpcParams,
readLeaseAllocateProviderFlags,
type LeaseRpcCommand,
} from '@agent-device/contracts/lease-scope';

Expand Down Expand Up @@ -183,9 +184,7 @@ export function buildHttpRpcPayload(
method: leaseRpcMethodForCommand(req.command),
params: {
...buildLeaseRpcParams(req, req.command, options),
...(req.command === 'lease_allocate' && typeof req.flags?.providerApp === 'string'
? { providerApp: req.flags.providerApp }
: {}),
...(req.command === 'lease_allocate' ? readLeaseAllocateProviderFlags(req.flags) : {}),
},
};
}
Expand Down
6 changes: 2 additions & 4 deletions src/daemon/server/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
DAEMON_HTTP_TENANT_HEADER,
} from '@agent-device/contracts/daemon-http';
import { readVersion } from '@agent-device/host-kit/version';
import { readLeaseAllocateProviderFlags } from '@agent-device/contracts/lease-scope';
import { sendRestJsonError, statusCodeForNormalizedError } from '../http-errors.ts';
import { tryHandleUploadHttpRoute } from '../upload-http.ts';
import { tryHandleDownloadableArtifactHttpRoute } from '../downloadable-artifact-http.ts';
Expand Down Expand Up @@ -316,10 +317,7 @@ function toLeaseDaemonRequest(
session: readStringParam(params, 'session') ?? 'default',
command,
positionals: [],
flags:
command === 'lease_allocate'
? { providerApp: readStringParam(params, 'providerApp') }
: undefined,
flags: command === 'lease_allocate' ? readLeaseAllocateProviderFlags(params) : undefined,
meta: {
tenantId: readStringParam(params, 'tenantId') ?? readStringParam(params, 'tenant'),
runId: readStringParam(params, 'runId'),
Expand Down
Loading
Loading