Repository navigation
refactor: declare the profile fields each provider consumes, once - #3168
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 32 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
The code in 54890d4 looks correct to me, and the one check on this PR is green. I did not run the eager-closure budgets, the layering scan or the test suites. I also did not run it against a live cloud provider. The change refuses before any provider contact, so I did not ask for that. Nothing here blocks merge. Cubic's review of this head and #3165 landing first are what remain before merge. One state I did not check: a lease that is active in the registry with no in-memory provider session, for example a recovered Limrun lease. There, a failed re-creation now keeps the lease until its TTL instead of releasing it. Could you confirm whether that state can happen? Not blocking, take or leave: the new Could the checker and type sit next to The four earlier inline threads from Cubic are fixed at this head, so you can resolve them: #3168 (comment), #3168 (comment), #3168 (comment) and #3168 (comment). |
54890d4 to
fe6d0e8
Compare
|
Now that #3165 has merged, I've rebased this onto The rebase surfaced two gate failures, each fixed in its own commit with no behaviour change:
On the recovered-lease question: that state can't happen.
The only way to get an active lease with no provider session is two allocations at once for the same run and client. While the first is still waiting on the provider, the second sees the lease as reused. If the second fails, keeping the lease is correct; releasing it would pull the lease out from under the first allocation, which would then return an inactive lease and leave its instance without an owner. A related pre-existing issue, outside this PR: in that same concurrent case, if the second Limrun allocation succeeds, The non-blocking restructure (checker and type in the contracts |
…r consumes Lease allocation forwards every provider profile field, but only some routes checked which ones a provider could act on. The connect builder and the AWS Device Farm session preparation refused BrowserStack device features, while the Limrun lease runtime, BrowserStack itself, and a remote-config profile skipped any check. A typed client asking Limrun for providerOsVersion '18.0' was therefore given whatever instance Limrun picked without an error. Each lease provider now declares, for every Cloud provider profile field, whether it consumes or refuses it. The declaration is a total record over CloudProviderProfileFields, so adding a field fails to build until every provider decides. One refusal, rejectRefusedProviderProfileFields from the new @agent-device/contracts/provider-profile-fields subpath, runs at lease preparation (the WebDriver session manager for BrowserStack and AWS Device Farm, and Limrun allocation) and at connect, so connect, leases.allocate and remote-config profiles all fail the same way with INVALID_ARGS naming the flag and the provider. This replaces the BrowserStack-only device-feature check. AWS Device Farm now also refuses the hub-only app, OS version, project and build flags it never read, and BrowserStack refuses the AWS Device Farm flags. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… too The daemon hands a run's repeat lease_allocate the lease it already holds, and both the WebDriver runtime and the Limrun runtime returned the live session for a known lease before checking the request's profile fields. A second allocation on the same run that set a refused field, such as awsProjectArn on BrowserStack, therefore succeeded. Both runtimes now refuse before reusing a live session. Because a refusal of that repeat request is a provider allocate failure, the lease handler no longer releases a lease it reused when allocate throws; doing so would end the run's lease and leave the session the first allocation created without an owner. The runtime's profileFields option is now required, and each provider definition passes its own declaration into createRuntime, so a provider that forgets to wire its declaration fails to compile instead of silently refusing nothing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…the profile field map The refusal's field-to-flag table and the flag column of BROWSERSTACK_DEVICE_FEATURE_SPECS spelled the same flags twice. PROVIDER_PROFILE_FIELD_FLAGS now lives beside CloudProviderProfileFields and is exported from @agent-device/contracts/remote, which the BrowserStack table already imports, so its rows read each flag from that map without growing the provider-webdriver eager closure. The refusal reads the same map. A command-registry test pins every entry to the CLI's canonical flag for the field. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…celed A repeat allocation reuses the run's live lease, and a provider refusal leaves that lease in place. A requester that hangs up also surfaces as an allocate rejection, because the request signal aborts with the typed cancellation, so the handler preserved the lease for a requester that would never release it, and the provider session from the first allocation lingered until the TTL sweep. Release the lease and its provider session through the gone-requester path when the request was canceled, reused or not. Only a refusal while the requester still waits preserves the reused lease. The refused-repeat test now also asserts the first allocation's provider session is still owned by the lease. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The refusal builder emits INVALID_ARGS with the refused flags in details.flags. Key the runtime, lease, connect and adapter tests on those typed details so rewording the message or a provider label does not break them. The builder's own unit tests still pin the message. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A refusal named only the canonical --provider-* flag, so a user who set the field through an alias such as --os-version was told to drop a flag they never typed. The flags bag keeps the field, not the spelling, so the message now lists every accepted spelling, for example "--provider-os-version (--os-version)". details.flags stays canonical. Contracts cannot depend on the command registry, so the aliases live in PROVIDER_PROFILE_FIELD_FLAG_ALIASES, and a command-registry test checks that map against the aliases the CLI parses. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's provider-only work pass and the reused-lease rules met in the lease_allocate handler and pushed it over the complexity threshold. Move the failure handling (release a new lease, keep a reused one, release a reused one whose requester left) into settleFailedAllocation. No behaviour change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
cloud-connect-profile.test.ts is over the test-file size tripwire and may not grow. The two new refusal tests go to cloud-connect-profile-fields.test.ts, which mirrors the profile builders they exercise. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Allocate and connect against an existing instance (LIM_IOS_INSTANCE_URL and token, no API key) accept the consumed --provider-app and refuse the rest, like a created instance. Co-Authored-By: Claude Opus 5.5 <[email protected]>
fe6d0e8 to
2386729
Compare
|
Rebased onto
One question I noticed but didn't change, since it's #3173's behaviour: on the attach path, |
|
This PR is ready at 2386729. The conflict from the earlier review (#3168 (comment)) is resolved, and all 16 checks pass on this head. There are no conflicts, and it needs a maintainer approval to merge. Not blocking, and you can take or leave these: the doc comment for I did not run the tests locally, and I did not re-review the four threads resolved before 54890d4. I did not run a live Limrun attach. A refused flag would fail before any provider client is created, so a unit test covers that route. |
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Thanks. In 3d27e71, the On Limrun's attached instances: yes, I'd leave that for the per-route validation. Today |
|
Rechecked at 3d27e71. The only change since 2386729 moves the |
Reverts 62e814c. The maintainer judged the connect-route check redundant: plugin resolve and lease allocation both refuse those fields, and callstack#3168 owns profile-field admission. Co-Authored-By: Claude Opus 5.5 <[email protected]>
callstack#3168 landed the provider profile-field declarations and their refusal on main. This PR carried an earlier copy of the same work, so the conflicts resolve to main's version throughout: rejectRefusedProviderProfileFields with alias-aware messages, PROVIDER_PROFILE_FIELD_FLAGS and its aliases in remote-config-fields, the BrowserStack, AWS Device Farm and Limrun declarations (including the attach-path refusal), settleFailedAllocation in the lease handler, and main's tests. What stays from this PR is the providerDeviceType field TestMu introduces: its flag map entry, a 'refused' row in each existing declaration, and a 'consumed' row in the test declarations. The BrowserStack device-type integration test is dropped; main's repeat-allocation test covers that refusal path. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* refactor(provider-webdriver): share hub upload and app-reference helpers BrowserStack's app upload, install adapter, --provider-app resolution, session-details URL artifacts, and orientation check were written for one vendor. A second hosted Appium hub needs the same mechanics with a different form field, reference scheme, and response shape, so move them into shared helpers and have BrowserStack use them: - webdriver-utils.ts: postHubAppUpload, createHubUploadApp, resolveHubAppReference, appFileUploadForm, asRecord, readProviderJsonBody, and requireProviderDeviceOrientation. - artifact-results.ts: urlArtifactFromDetails. - browserstack.ts: resolveBrowserStackAppReference, moved out of provider-definitions.ts. Two small BrowserStack behaviour changes come with the shared code: - An upload response that is not JSON (a gateway error page, an empty body) now fails with a typed COMMAND_FAILED that carries the HTTP status, instead of a raw JSON SyntaxError. - The http(s) scheme of a --provider-app URL is matched case-insensitively, so HTTPS://... is passed through to the hub rather than being treated as a local path. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(provider-webdriver): bound and type session-details lookups The BrowserStack session-details lookup behind `artifacts` had no deadline, so a stalled API call could hang the command indefinitely. A transport failure or a body that was not JSON surfaced as an untyped fetch or SyntaxError, and a JSON array passed the object check and was read as session details. Add fetchProviderSessionDetails to webdriver-utils.ts and use it for BrowserStack. It sends basic auth with a 15 second deadline and reports every failure as COMMAND_FAILED: a timeout or network error with a retry hint and the original error as its cause, and a non-2xx answer or a body that is not a JSON object with the HTTP status and the parsed response. Connection verification gets the same treatment through fetchProviderVerificationJson, which BrowserStack now uses in place of its private fetch. Behaviour is unchanged: 401/403 is UNAUTHORIZED with a credential hint, any other HTTP failure points at the provider's service status, and a transport failure points at network access. A new test pins the two non-credential hints. sameOsVersion moves alongside it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(provider-webdriver): add TestMu AI emulator and simulator provider Add `testmu`, a hosted WebDriver provider for TestMu AI (formerly LambdaTest) virtual devices: Android emulators and iOS simulators behind the TestMu AI Appium hub. It follows the BrowserStack shape: `connect testmu` verifies and saves a local profile, and `open` creates the hosted session. The service hostnames still carry the lambdatest.com domain. connect - Reads LT_USERNAME and LT_ACCESS_KEY, the variables TestMu AI SDKs use. - Checks the device and OS version against the public virtual-device catalog. The match is exact because the hub rejects `18` for a device listed as `18.0`; the error lists the versions the device offers. - Uses the authenticated app listing as the credential check, and looks an lt:// id up in the list for the session's runtime (`emulator` or `simulator`). An id that is not listed is reported as configured, since TestMu AI validates it at session creation. - Verifies against TESTMU_API_ENDPOINT when it is set, as the runtime does, and composes catalog and listing URLs with URL so a base or override that carries a query keeps it. - Never creates a session. Sessions - Standard Appium keys stay `appium:`-prefixed; everything vendor- specific goes in `lt:options`, merged per key with any configured `lt:options`. `isRealMobile: false` and `w3c: true` are applied last, so configuration cannot move the session to another device pool or off the W3C dialect agent-device speaks. - `appiumVersion` is sent only when --provider-appium-version pins one; otherwise TestMu AI starts its default server for the device. - Orientation, geo-location, timezone, Appium version, language, and locale map onto `lt:options` through a table. The BrowserStack-only network-profile, custom-network, and no-resign flags are refused by flag name at connect and at session preparation, instead of being silently dropped. Apps - --provider-app takes an lt:// id, an http(s) URL, or a local path. The hub only accepts lt:// references, so a URL is handed to the upload API to fetch (`storage=url`) and a local file is uploaded. - An unzipped iOS `.app` directory is rejected before any request, with a hint to zip it. - Only a well-formed lt:// reference or app id in the upload response counts as success. Artifacts - Read from the session-details API through the shared bounded lookup. A 404 reads as pending until TestMu AI publishes the details. `console_logs_url` is the device log on virtual devices. - Session video, Appium, network, and command logs, screenshots, and the dashboard link are returned once at least one artifact URL exists. TESTMU_WEBDRIVER_ENDPOINT, TESTMU_APP_UPLOAD_ENDPOINT, and TESTMU_API_ENDPOINT redirect the endpoints. The session, upload, verification, and device-feature modules are loaded with dynamic import, so the package entry's eager module graph does not grow. The CLI reaches the device-feature checks through a new `./testmu-device-features` subpath export. A .fallowrc entry covers the exports that are read only through the dynamic import. `connect testmu` is listed in the connect usage, the remote help topic, and the artifacts provider description. Co-authored-by: gautam-jain-dev <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(provider-webdriver): support TestMu AI real devices TestMu AI serves real devices and virtual devices from the same Appium hub; `lt:options.isRealMobile` selects the pool. Add `--provider-device-type real|virtual` to choose it. The default is `virtual`, so existing `testmu` profiles and sessions keep their current behaviour. The value is a cloud provider profile field like the others. It is defined in contracts as PROVIDER_DEVICE_TYPES and carried through the lease_allocate projection, request overrides, the remote-config schema, `connect`, the CLI request flags, and the session doctor's provider keys. It reaches session preparation from a `connect testmu` profile and from `client.leases.allocate({ providerDeviceType })`. For `real`, TestMu AI sessions: - Set `isRealMobile: true`. It is applied after any configured `lt:options`, so configuration cannot switch pools. - Upload through the real-device upload API. It has its own override, TESTMU_REAL_DEVICE_APP_UPLOAD_ENDPOINT; TESTMU_APP_UPLOAD_ENDPOINT keeps redirecting virtual-device uploads only. An `.app` directory is refused with a hint to pass a signed .ipa. - Verify against the real-device catalog (`capability/generator?isVirtualDevice=false`). It lists devices directly under the platform key rather than under `app.devices`. The OS version must still match exactly, and real iOS devices are listed by major version, such as `18`. A catalog response without the expected pool shape fails with a typed error. - Look an lt:// id up in the real-device app list (`type=android` or `type=ios`), the same way virtual uploads are looked up under `emulator` or `simulator`. BrowserStack, AWS Device Farm, and Limrun refuse the flag at connect. BrowserStack and AWS Device Farm also refuse it at session preparation, which the typed client and hand-written profiles reach without `connect`. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(provider-webdriver): upload the archive of materialized iOS builds Install from a remote source materializes an iOS build by extracting the `.app` bundle from a zipped simulator build or an .ipa. The WebDriver deployment runtime handed that extracted `.app` directory to the provider's uploader. Hosted upload APIs take a file, not a directory, so the upload could not succeed. When the materialized artifact is an iOS `.app` extracted from a `.zip` or `.ipa` archive, upload the archive it came from. Every other case uploads the installable path as before: no archive, an archive of another type, or an Android build. A provider without an uploader still installs the extracted bundle path. The bundle id and launch target hints are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: add the TestMu AI device cloud guide Add a TestMu AI guide next to the BrowserStack and AWS Device Farm ones and link it from the sidebar. It covers credentials and `connect`, the exact device and OS version match, app references and uploads, device features, real devices with `--provider-device-type real`, the CLI and Node.js client workflows, artifacts, and endpoint overrides. List TestMu AI among the device clouds in the README, the device clouds overview, the client API page, and the command reference. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(daemon): start a lease's TTL when its allocation completes The registry records a lease, and stamps its expiry, before the lease lifecycle provider allocates the session behind it. Hosted providers can spend 30-80 seconds creating that session, so a lease on the default one-minute inactivity window was expired, or nearly so, by the time its client received it: the next command failed with "Lease is not active", release reported nothing to release, and the paid provider session was left running. The expiry sweep could also reap the lease while the provider was still allocating. Hold a lease work pass for the duration of the provider allocation, the same protection admitted request work gets. The lease cannot expire underneath the allocation, and ending the pass while the requester is still waiting renews the lease for its own TTL from that moment. The response now carries the renewed lease. A requester that hung up still protects nothing, and its allocation is released as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(provider-webdriver): declare the profile fields each provider consumes Lease allocation forwards every provider profile field, but only some routes checked which ones a provider could act on. The connect builders and the BrowserStack/AWS session preparation refused flags they did not own, while the Limrun lease runtime and a remote-config profile skipped that check. A typed client asking Limrun for providerDeviceType 'real' was therefore given a simulator without an error. Each lease provider now declares, for every Cloud provider profile field, whether it consumes or refuses it. The declaration is a total record over CloudProviderProfileFields, so adding a field fails to build until every provider decides. One refusal, derived from that declaration, runs at lease preparation (the WebDriver session manager for BrowserStack, AWS Device Farm and TestMu AI, and Limrun allocation) and at connect, so connect, leases.allocate and remote-config profiles all fail the same way with INVALID_ARGS naming the flag and the provider. This replaces the per-provider TestMu-only, TestMu-unsupported and BrowserStack-only checks, the hand-copied complement of the BrowserStack feature table, and the ./testmu-device-features package subpath. AWS Device Farm now also refuses the hub-only app, OS version, project and build flags it never read. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(provider-webdriver): let the Apple materializer name the uploadable file The WebDriver deployment runtime guessed what to upload for a materialized iOS build from file extensions: an extracted .app with a .zip or .ipa archive uploaded that archive. The archive it saw is the outermost one the source arrived as, so a URL .zip wrapping an .ipa uploaded the wrapper zip instead of the .ipa, and a zip holding a .app.tar.gz uploaded a zip with no app bundle at its top. Materialization now records the archive an installable was extracted from directly, and the Apple materializer declares the file a hosted provider uploads: the .ipa itself, or the zip a simulator .app was extracted from. It names nothing when no such file exists. The deployment runtime uploads the declared file and otherwise the installable path, with no inference of its own. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(provider-webdriver): tighten app-reference, endpoint and upload input handling Several inputs reached a hosted provider in a shape it could not use, or failed with an untyped error: - An empty `lt://` passed as a TestMu AI app reference and reached session creation. A reference is now validated against the app-id grammar, and an invalid one is INVALID_ARGS on connect and at session preparation. - `LT://APP1` or `BS://id` was treated as a file path. App schemes now match case-insensitively and are canonicalized to the spelling the hub accepts. - A directory given as `--provider-app` reached the upload and failed with a raw EISDIR; it is now INVALID_ARGS with a hint. - A missing path given straight to the TestMu AI upload failed with a bare ENOENT; it now gets the same typed refusal as a directory. - Session-detail URLs were built by string concatenation, so a query on TESTMU_API_ENDPOINT or the BrowserStack details endpoint swallowed the route. Routes are now appended to the URL path, keeping the query. - A whitespace-only artifact URL was reported as a ready artifact; it is now treated as absent, and URLs are trimmed. - A 2xx connection-verification answer that is not JSON was reported as a network failure. It is now COMMAND_FAILED with the HTTP status, and each hub supplies its own service-status hint. The connect help and the TestMu AI guide now say what connect checks for `--provider-app` and what is only validated when the session is created. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(provider-webdriver): exercise in-flight deadlines and aborts for real The BrowserStack session-details timeout test threw a prebuilt TimeoutError from the fetch stub, so it passed even with the deadline removed. The fetch now stays pending until the signal the lookup armed aborts, and the test checks that signal is the 15 second deadline. The TestMu AI upload cancellation test aborted before the upload reached fetch, so it never covered a request in flight. It now waits for the stub to start before aborting. The connect suites shared a copied connectWithGeneratedProviderProfile helper; it now lives in the shared test utilities. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(provider-webdriver): refuse profile fields on a repeat allocation too The daemon hands a run's repeat lease_allocate the lease it already holds, and both the WebDriver runtime and the Limrun runtime returned the live session for a known lease before checking the request's profile fields. A second allocation on the same run that set a refused field, such as providerDeviceType on BrowserStack, therefore succeeded. Both runtimes now refuse before reusing a live session. Because a refusal of that repeat request is a provider allocate failure, the lease handler no longer releases a lease it reused when allocate throws; doing so would end the run's lease and leave the session the first allocation created without an owner. The runtime's profileFields option is now required, and each provider definition passes its own declaration into createRuntime, so a provider that forgets to wire its declaration fails to compile instead of silently refusing nothing. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(provider-webdriver): validate and canonicalize app references at connect Connect only checked that an lt:// or bs:// reference had something after the scheme, so `lt://a b` and `lt://a/b` were saved and failed only when the hub created the session. Both hubs' reference grammars now live in the light providers module, and connect, session preparation and the TestMu AI upload reader all validate against them. Connect also verified the raw spelling typed on the command line: the generated profile stored `lt://APP1` for `LT://APP1`, but the flags handed to verification were the raw CLI flags laid over the profile, so the uploaded-app lookup missed and reported a local artifact. The canonical reference now wins in the flags verification reads. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(provider-webdriver): refuse a directory before any hosted upload A materialized build that names no uploadable file falls back to its installable path, which for iOS is the extracted .app directory. The deployment runtime handed that to the hub's uploader, and BrowserStack's read it as a file and failed with a raw EISDIR. The deployment runtime now refuses a directory before calling any hub's uploader, with INVALID_ARGS naming the provider and path and a hint to zip the .app or pass the .ipa, .apk, or .aab. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(cli): refuse a non-URL install-from-source source with a typed error install-from-source sends its positional as a URL source. A local path reached the iOS trusted-host check, where `new URL()` threw a TypeError that surfaced as an untyped UNKNOWN "Invalid URL". The CLI now refuses a positional that is not an http(s) URL with INVALID_ARGS and points at `install <app> <path>` for local builds, and the trusted-host check reports an unparsable source as INVALID_ARGS instead of throwing. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat: support provider-owned plugin connections and WebDriver adapters * chore(gates): expose the optional WebDriver plugin configuration * feat: load TestMu from its separately installed provider plugin * chore(gates): declare TestMu plugin build dependencies and analysis entrypoints * fix: retain plugin tests through development-only package exports * chore(gates): track TestMu packaging fixtures and workspace test dependencies * chore(gates): smoke the optional packed WebDriver plugin SDK * perf: load the shared WebDriver engine on demand * chore(gates): bound the optional plugin SDK declarations * fix: consolidate TestMu plugin helper imports * fix(testmu): trim credentials and log URLs, canonicalize lt:// everywhere Whitespace-only LT_USERNAME/LT_ACCESS_KEY now fail as missing in the runtime, matching connect. Verification of a hand-authored profile, upload responses, and app-list ids share one case-insensitive lt:// canonicalization, and a blank console_logs_url falls back to device_logs_url. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(testmu): export credentials in help and note the fixed app listing The help flow's bare assignments did not reach the next agent-device process. The endpoint overrides do not move the app listing connect uses for the credential check, so say so instead of implying a private deployment is fully redirectable. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(kernel): require a real Error behind the AppError brand A plain object carrying the global brand symbol passed instanceof AppError and left normalizeError with no code or message. Package copies share the realm's Error.prototype, so the plugin-copy case still holds. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(plugins): refuse a primitive factory result as an invalid plugin 'webDriver' in <primitive> threw a raw TypeError past the loader's INVALID_ARGS validation. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(plugins): archive into a fresh zip, not over a stale one zip -r updates an existing archive in place, so leftover entries from an earlier attempt would ride along in the upload. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(connect): look plugin capabilities up in the caller's environment isConnectProviderName already took an env; connectionProviderCapabilities read process.env, so under another plugin home the two disagreed and a plugin provider fell back to the builtin capability shape. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(kernel): type the AppError brand lookup on a narrowed Error Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(plugins): reserve provider ids from one list assertUniquePluginProviders merged RESERVED_PLUGIN_PROVIDERS into whatever its callers passed, and the daemon passed the overlapping DEFAULT_PROVIDER_RUNTIME_REQUIRED_IDS. Every caller now passes RESERVED_PLUGIN_PROVIDERS, which takes the WebDriver ids from CLOUD_WEBDRIVER_PROVIDERS, and a test pins every bundled runtime id into it. plugins add/update no longer loads the provider runtimes. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(plugins): name the WebDriver factory result check Keeps instantiateProviderPlugin under the Fallow complexity threshold after the object guard. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore(fallow): stop suppressing statically imported TestMu helpers connection.ts imports readTestMuDeviceFeatureFields and readTestMuDeviceType statically, so Fallow sees them; only the lazily read buildTestMuDeviceFeatureCapabilities still needs the exemption. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test: allocate through a WebDriver plugin and assert typed ownership The WebDriver plugin fixture now declares its profile fields and the test allocates a session through the shared engine, refusing a field it marks refused before any request. The refused repeat allocation also asserts session-1 still resolves to the live lease, and the Limrun refusal checks details.provider and details.flags instead of copy. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(connect): refuse a WebDriver plugin's refused fields before resolve Builtin connect routes run rejectRefusedProviderProfileFields against their declaration; the plugin route left it to the plugin. A { webDriver } plugin already hands core a total profileFields declaration, so the connect route now applies it before the plugin's resolve. Raw runtime plugins carry no declaration and still validate their own flags. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(testing): say when to run the packed provider plugin check check-provider-plugin.mjs needs pnpm and a built core, which the Node 22.12 package lane cannot run and whose script body CI credits verbatim, so it is a manual pre-push check for plugin package changes. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: document the plugin package check in its script, not testing.md testing.md is over the agent-guidance byte budget with the note, so the how and when now sit in the script header. Co-Authored-By: Claude Opus 5.5 <[email protected]> * revert: let resolve and allocate own plugin profile-field refusal Reverts 62e814c. The maintainer judged the connect-route check redundant: plugin resolve and lease allocation both refuse those fields, and #3168 owns profile-field admission. Co-Authored-By: Claude Opus 5.5 <[email protected]> * revert: keep the AppError brand check as it was Reverts c553ed7 and aab5684. Plugins are trusted in-process code, so a forged brand is not a threat the maintainer wants handled here. Co-Authored-By: Claude Opus 5.5 <[email protected]> * revert: drop the plugin host archive fix with the apple host block Reverts 1d52862; ProviderPluginHost.apple is removed next. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(plugins): remove the unused apple block from the plugin host No plugin calls host.apple, and archiveDirectory copied the Limrun dependency's zip helper. A later plugin that needs Apple packaging can share one extracted helper with Limrun. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(webdriver): one owner for lease-flag and credential readers requireRequest, requireRequestPlatform, requireFlag, readFlag and requireEnv move from provider-definitions.ts into webdriver-utils.ts and are exported from the provider-webdriver/plugin subpath; the TestMu plugin drops its copies. requireEnv keeps the trim-empty rule, so a whitespace-only BrowserStack credential now also fails as missing. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(testmu): import session and device-feature modules statically connection.ts already imports them statically, so the dynamic imports in plugin.ts saved nothing; the Fallow exemption for the lazy read goes with them. Co-Authored-By: Claude Opus 5.5 <[email protected]> * build(testmu): typecheck the plugin against core source, not dist CI typechecks before it builds, so agent-device/plugins and agent-device/plugins/webdriver resolved to dist types that did not exist yet and ProviderPluginHost became any. Both tsconfigs map the two specifiers to src/sdk, as examples/sdk does; provider-testmu maps through its workspace link so the files stay outside its rootDir. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore(fallow): credit the testmu plugin's type-only agent-device link With the tsconfig mapping to core source, Fallow resolves the plugin's agent-device/plugins imports to src/sdk files and reported the devDependency unused, which a clean checkout surfaced. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(testmu): assert only Limrun's device-type refusal at connect Main's refusal names a field's aliases too, so the combined message this test matched no longer exists, and main already covers Limrun refusing the OS version. Keep the part this PR adds: --provider-device-type is refused by Limrun. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(providers): share the lower-case scheme rule for hub app references canonicalTestMuAppReference repeated the rule canonicalBrowserStackAppReference applies: a hub matches only the lower-case scheme, so BS:// and LT:// are rewritten to bs:// and lt://. Both now call canonicalSchemeReference from the dependency-free provider-webdriver/providers subpath, so neither eager closure grows. Behaviour is unchanged: BrowserStack still returns undefined without the scheme, and TestMu still returns the value as given. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(provider-webdriver): export the plugin's webdriver-utils readers in one block Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(providers): refuse a lease when the daemon holds other plugin credentials A provider plugin can declare agentDevicePlugin.credentialVariables in its manifest. providerCredentialFingerprint falls back to that declaration for providers without a built-in reader, so the client (shell env) and the daemon (startup env) both fingerprint plugin credentials without loading plugin code. TestMu declares LT_USERNAME and LT_ACCESS_KEY, which brings it under the provider-credentials-changed refusal from #3207. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(plugins): split manifest connection and credential checks Keeps readPluginManifest under the fallow complexity threshold, and types the manifest fixture's version so the typecheck passes. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: explain installation of the TestMu package * chore(gates): align TestMu package with workspace releases * docs: explain daemon restart after plugin installation * docs: show the complete plugin connection manifest * chore(gates): deduplicate the merged package boundary fixture import --------- Co-authored-by: Anurag Sharma <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Michał Pierzchała <[email protected]>
Summary
Split out of #3118 and stacked on #3165. Please review only the top three commits. This is the single consumed-field admission you asked us to keep in core.
@agent-device/contracts/provider-profile-fieldsexport holds:ProviderProfileFieldDeclaration, a full consumed/refused record over the profile fields.rejectRefusedProviderProfileFields.CLOUD_WEBDRIVER_PROFILE_FIELDS) and Limrun (LIMRUN_PROFILE_FIELDS). The runtime'sprofileFieldsis required.leases.allocate, remote-config, and repeat allocation of a live lease.PROVIDER_PROFILE_FIELD_FLAGSis the single field-to-flag map, and the BrowserStack device-feature specs derive their flags from it.Behaviour changes (nit [5]): flags that were silently ignored are now refused:
--provider-app,--provider-os-version(and its--os-versionalias),--provider-projectand--provider-build.--aws-*.--provider-app.None of the documented examples or help text use a refused flag.
Validation
pnpm check:affected --runpasses, including replay-compat with release tags fetched.eager-closure-budgetspasses.🤖 Generated with Claude Code