feat: add direct Doublespeed provider runtime - #2112
Conversation
Adds `agent-device connect doublespeed`, a direct iOS-simulator provider backed by the Doublespeed Mac fleet. The provider mirrors the Limrun package: lease lifecycle with label-selector recovery, a session-API interactor, content-addressed app deployment, and an ADR-0019 platform runtime owner with durable app-log recovery. No new external dependency; the kernel snapshot provenance table gains the `doublespeed-ios-tree` producer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Not ready at
|
|
Current exact head |
Summary
Adds
agent-device connect doublespeed, a direct provider runtime for Doublespeed iOS simulators (a Mac mini fleet exposed over HTTPS, billed per second). It follows the Limrun provider's shape end to end so operators get the same workflow:What the provider owns:
allocatecreates a labelled simulator session and waits until its session API is up;releaseends it;recoverExpiredLeasefinds orphans by theprovider/leaseIdlabels and ends them, exactly like the Limrun label-selector recovery.id/label/text/value), type/fill, scroll, home, portrait/landscape orientation, screenshot, and accessibility-tree snapshots stamped{ backend: 'xctest', producer: 'doublespeed-ios-tree' }(new producer added to the kernel provenance table)..appdirectories are zipped by the host adapter, published once per organization through a content-addressed asset endpoint (sha256; a second install of the same build skips the upload), then installed and verified against the post-install inventory.doublespeed-log-pollerdescriptor with reattach/cleanup), network dump from app logs, app inventory, app state (foreground bundle id), lifecycle, deployment, and every interaction cell above. Unsupported cells are stated as facts with hints — back, alerts, clipboard, keyboard, settings, app switcher, gesture plans, port reverse, recording — and refuse at admission rather than mid-execution.401/403→UNAUTHORIZEDwithout echoing the key,402→ a credits hint. Generated profiles store no secret;DOUBLESPEED_DEVICE(simulator model) andDOUBLESPEED_API_URLare read from the environment.No new external dependency: both clients are small
fetchwrappers, so the package's closure adds nothing to the npm unpacked size beyond its own source, and the CLI's eager import closure is unchanged (the package's entry is budgeted at 28 modules, alongside Limrun's 29).Touched: 60 files (+5,067/−31). Production growth is ~2,800 lines in the new package plus ~200 lines of root wiring (
provider-device-runtimes, connect adapters/profile/policy, help text); the rest is tests, fixtures, docs and the lockfile. Scope stayed inside the provider family: the only shared-surface edits are the kernel producer union and the composition root.Size note (≥700 net production lines)
The growth is a second provider package rather than new abstractions. Nine modules are near-copies of their Limrun siblings (
app-log-poller,app-log-descriptor,app-log-runtime,facts-runtime,interaction-operations,deployment-runtime,lifecycle,runtime-instance, fixtures) —pnpm check:fallowreports ~590 mirrored lines but passes. I kept the copy deliberately: lifting a provider-neutral "polled app-log owner" intocapture-kitwould have meant refactoring the Limrun package in the same PR, which crosses the one-family rule. I'd be glad to do that extraction as a follow-up once a second consumer exists inmain, if maintainers prefer it that way.Validation
pnpm check:quick,pnpm check:layering(149/149),pnpm check:fallow --base origin/main(clean apart from the duplication note above),pnpm check:production-exports,pnpm build, andpnpm check:affected --base origin/main --runare green at this head. Package boundary and eager-closure gates were extended with rows for the new package.packages/provider-doublespeed(lifecycle denominator across every Apple leaf/kind/target and every other family, stale-session admission, exact-owner recovery, poller dedup/abort/rollback, install verification with eventually consistent inventory, asset publish flow, API error classification with no key leakage) plus root tests for composition (DOUBLESPEED_API_KEY),connect doublespeedprofile generation (iOS-only, rejects--platform android), help topics, and snapshot provenance.8dad6500against a real Doublespeed simulator (iPhone 16, iOS 18.6), from the built CLI with an isolated--state-dir:connect(verified, deferred device) →install com.doublespeed.HelloWorld ./HelloWorld.app(≈50 s including simulator boot and the first upload; returns the bundle id) →open --relaunch→snapshot -i(9 nodes: application/window/image "Globe"/text "Hello, World!"/text-field "World") →click 'label="Hello, World!"'(tapped 197,435 on the 393×852 reference) →screenshot(1179×2556 PNG showing the app) →apps(HelloWorld (com.doublespeed.HelloWorld)) →appstate(foreground bundle id) →close(provider.doublespeedSimulatorIdreturned, simulator ended, 66 billable seconds) →disconnect. The session was closed and released; no simulators remained allocated afterwards.Known gaps, all stated as unavailable facts:
back(no navigation-back primitive on the session API yet), alert read/accept/dismiss, clipboard, keyboard status/dismiss, gesture plans, port reverse (use a public Metro/DevTools URL, as with Limrun iOS), screen recording, provider artifacts.