Skip to content

Commit 058826d

Browse files
thymikeeclaude
andcommitted
docs(plugins): document plugin contract v2, the inventory and ADR 0034
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
1 parent 5645905 commit 058826d

6 files changed

Lines changed: 159 additions & 28 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Provider plugins have a new contract and one provider registry. A package exports a factory at `./agent-device` that returns `{ apiVersion: 2, provider, credentials, connection?, setup(host) }`, typed by `AgentDevicePluginFactory` from `agent-device/plugins`; the store calls it with the package's `config.json` options and a JavaScript host can call it directly. Bundled providers, installed plugins, and embedder plugins register in that order through one registry that claims each provider id once: a duplicate id is recorded as failed, and `cloud` and `proxy` stay reserved. A plugin that fails to load, validate, or set up no longer stops the daemon; it is listed as `failed` with a typed `error.reason`, and the other providers still register. The inventory is published in `daemon.json` and shown by the new `agent-device daemon status [--json]`; `plugins list --json` works without a daemon and reports a running daemon's inventory under `daemon`. A client now takes provider credential variable names from that inventory, narrowed per lease backend, and falls back to v1 manifests. Version 1 plugins (`agentDevicePlugin.apiVersion: 1`) keep loading. A Limrun credential fingerprint now hashes the exact variable values, so padding whitespace around a value counts as a change like it does for BrowserStack, and a partial Limrun instance set shows in `daemon status` and as `provider_plugin_failed` in `daemon.log` instead of `provider_runtime_skipped`.
56
- Add `uninstall <app>` and `client.apps.uninstall({ app })` to remove an installed app on adb-backed Android targets, iOS simulators, and CoreDevice iOS devices. Both report `wasInstalled`. A package name or bundle id that is not installed succeeds with `wasInstalled: false`. Other targets, Appium and Limrun sessions included, refuse it before anything runs.
67
- Add `settings mobile-data on|off` for adb-backed Android targets. It switches mobile data through `svc data` and reports the `mobileData` state stored in the global `mobile_data` row; multi-SIM read-back is not covered yet. Every other target refuses it before anything runs, and `--app` is refused because the switch is device-wide.
78
- On iOS, a `fill` that runs under a degraded XCTest snapshot channel now aims at a point, including ref, selector and replay fills. When the runner identifies no text input under that point, it refuses with `NO_TEXT_INPUT_AT_POINT` (`error.details.reason: no_text_input_at_point`, `dispatched: no`) before dispatching its focus tap, and no field changes; when the input lookup itself cannot answer, it refuses with `TEXT_INPUT_PROBE_UNAVAILABLE` (`error.details.reason: text_input_probe_unavailable`). Previously such a fill cleared and typed through the app-level first responder, so it could rewrite a field the caller never named, or report a clear failure after emptying it. A ref or selector fill whose rect centre misses the real field (an outer container, a non-input node) now needs a point inside the input's own rect.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# 0034. Provider plugin registry
2+
3+
Status: accepted. Umbrella #3747; contract and registry #3755.
4+
5+
## Rules at a glance
6+
7+
- One contract. A provider plugin is a factory that returns `{ apiVersion: 2, provider, credentials,
8+
connection?, setup(host) }`. `setup` returns `{ runtime, platformModule, dispose? }` or
9+
`{ webDriver, dispose? }`. A `{ webDriver }` result stays a data descriptor the host interprets.
10+
- One registry. Sources compose in the order built-in, store, embedder. Built-in providers use the
11+
same shape as an installed plugin, and one function validates what every factory returns.
12+
- Plugins import only types from agent-device. The type-only contract is `agent-device/plugins`;
13+
there is no runtime helper, and the contract does not depend on a plugin's module URL.
14+
- The first registration of a provider id claims it, even when that registration fails. Later ones
15+
are recorded as failed. `cloud` and `proxy` are routing identities no plugin can claim.
16+
- A plugin that fails to enumerate, load, validate or set up is recorded and skipped. The daemon
17+
starts with the rest. The inventory lists every plugin as `active` or `failed`, with a typed
18+
`error.reason`, and is published in `daemon.json` and by `agent-device daemon status`.
19+
- The plugin set is fixed for a daemon's lifetime. A plugin change takes effect on the next daemon.
20+
- v1 plugins (`agentDevicePlugin.apiVersion: 1`) without a `./agent-device` export are dispatched by
21+
their manifest before any code runs and load through an adapter until 0.22 removes them (#3759).
22+
A package that keeps the manifest and also exports `./agent-device` loads as v2; hosts without
23+
v2 still read the manifest.
24+
25+
## Context
26+
27+
Three things loaded providers: a hand-kept `RESERVED_PLUGIN_PROVIDERS` list, a second list of
28+
providers the daemon must have a runtime for, and a store that treated one bad plugin as a daemon
29+
startup failure. Each provider kind also needed its own path for credential fingerprints and for
30+
`connect` capabilities, which read manifests the bundled providers do not have. An embedding host
31+
had to fake the store to load a plugin from its own dependencies (#3747).
32+
33+
## Decisions
34+
35+
- **A factory takes options.** The store calls it with `config.json` `plugins[name].options`; a JS
36+
host calls it directly with its own, such as credentials. A plugin that takes credentials as
37+
options declares no environment variables for them, so they stay out of the environment
38+
fingerprint.
39+
- **A package exports its entry as `./agent-device`.** The host resolves it with import
40+
conditions and falls back to the package root only when the subpath is absent. A subpath whose
41+
import or setup throws never falls back. A v2 package needs no manifest.
42+
- **The registry owns identity, not the retired lists.** Duplicate ids and bundled ids are one
43+
question answered by claim order. Credential variables, per-backend narrowing and connect
44+
capabilities are read from the registry's plugins, not from manifests. A v1 manifest still
45+
supplies them for a v1 plugin.
46+
- **Failure is data.** A failed plugin is an inventory entry, not an exception. Behavior keys on
47+
`error.reason`, never on the message.
48+
- **A client reads a daemon's inventory from `daemon.json`.** The client credential fingerprint
49+
takes variable names from it and falls back to v1 manifests. A v2 plugin declares its connect
50+
capabilities in code, so a client loads the store, without setting up a runtime, before it reads
51+
them.
52+
53+
## Daemon ownership
54+
55+
The target is that starting or replacing a daemon is an explicit act, not a side effect of a client
56+
call. Today a client call can still start a daemon; #3754 makes that explicit for the client
57+
lifecycle, and #3757 adds `agent-device/host`, where an embedder owns its daemon and passes its
58+
plugins as the third source.
59+
60+
## Alternatives refused
61+
62+
- **Keep the manifest as the contract.** It cannot describe options-dependent credentials, and a
63+
bundled provider has no package to hold one.
64+
- **A runtime helper such as `definePlugin`.** It would make plugins depend on a runtime export of
65+
agent-device and its version, which is what a type-only contract avoids.
66+
- **Fail the daemon on a bad plugin.** One broken optional package then takes every provider down.

‎docs/adr/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
| [0031 macOS Native App Backend](0031-macos-native-app-backend.md) | `AGENT_DEVICE_MACOS_APP_BACKEND`, driving macOS app sessions without XCTest Automation Mode, why pointer actions are accessibility actions only, and the ghost cursor |
3636
| [0032 Layering Graph Engine](0032-layering-graph-engine.md) | evaluating replacement of `scripts/layering/` graph rules with dependency-cruiser or fallow `boundaries`, why graph-based layering checks share one replaceable import model, the measured edge-set and parity gaps, and the revisit triggers |
3737
| [0033 Collocation decision table](0033-collocation-decision-table.md) | moving or adding a file listed in umbrella #3276 §5, re-proposing a move that table refused, reading a zone's collocation decisions |
38+
| [0034 Provider plugin registry](0034-provider-plugin-registry.md) | provider plugins, the `agent-device/plugins` contract, plugin sources and order, duplicate provider ids, plugin failure isolation, the daemon plugin inventory and `daemon status` |
3839

3940
ADRs record *why*; the registries and gates they describe are the living source of truth — when
4041
prose and a registry disagree, the registry wins and the ADR needs a follow-up.

‎website/docs/docs/build-an-integration.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Entry points for integrations:
8585
- `agent-device/plugins/webdriver`
8686
- experimental `WebDriverPluginOptions` for providers using the shared engine.
8787
- `agent-device/plugins`
88-
- experimental factory context: `ProviderPluginHost`; see [provider plugins](./plugins.md).
88+
- experimental plugin contract: `AgentDevicePluginFactory` and `ProviderPluginHost`; see [provider plugins](./plugins.md).
8989

9090
## Android ADB providers
9191

‎website/docs/docs/limrun.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ An agent in a sandbox can drive an instance that someone else created, such as t
2929
| iOS | `LIM_IOS_INSTANCE_URL` (`status.apiUrl`), `LIM_IOS_INSTANCE_TOKEN` (`status.token`) |
3030
| Android | `LIM_ANDROID_INSTANCE_URL` (`status.apiUrl`), `LIM_ANDROID_INSTANCE_TOKEN` (`status.token`), `LIM_ANDROID_INSTANCE_ADB_URL` (`status.adbWebSocketUrl`) |
3131

32-
The `lim` CLI reads the same variables, so one set works for both tools. Set all of a platform's variables or none. With a partial set, `connect` fails, and the daemon disables Limrun and records `provider_runtime_skipped` in `daemon.log`.
32+
The `lim` CLI reads the same variables, so one set works for both tools. Set all of a platform's variables or none. With a partial set, `connect` fails, and the daemon registers without Limrun, lists it as `failed` in `agent-device daemon status`, and records `provider_plugin_failed` in `daemon.log`.
3333

3434
```bash
3535
export LIM_IOS_INSTANCE_URL=...

0 commit comments

Comments
 (0)