Skip to content

feat: add direct Doublespeed provider runtime - #2112

Open
hassantsyed wants to merge 1 commit into
callstack:mainfrom
hassantsyed:feat/doublespeed-provider
Open

feat: add direct Doublespeed provider runtime#2112
hassantsyed wants to merge 1 commit into
callstack:mainfrom
hassantsyed:feat/doublespeed-provider

Conversation

@hassantsyed

Copy link
Copy Markdown

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:

export DOUBLESPEED_API_KEY=...
agent-device connect doublespeed --platform ios
agent-device install com.example.app ./build/Example.app   # allocates the simulator, uploads the bundle once
agent-device open com.example.app --relaunch
agent-device snapshot -i
agent-device click 'label="Continue"'
agent-device close                                         # releases (and stops billing) the simulator
agent-device disconnect

What the provider owns:

  • Lease lifecycle: allocate creates a labelled simulator session and waits until its session API is up; release ends it; recoverExpiredLease finds orphans by the provider/leaseId labels and ends them, exactly like the Limrun label-selector recovery.
  • Interactor over the session's JSON API: open (bundle id or deep link), close, tap, long press, double tap, selector taps (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).
  • App deployment: .app directories 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.
  • Platform-runtime owner (ADR-0019): facts + bound operations for app logs (durable doublespeed-log-poller descriptor 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.
  • Connect verification: a read-only list call; 401/403UNAUTHORIZED without echoing the key, 402 → a credits hint. Generated profiles store no secret; DOUBLESPEED_DEVICE (simulator model) and DOUBLESPEED_API_URL are read from the environment.

No new external dependency: both clients are small fetch wrappers, 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:fallow reports ~590 mirrored lines but passes. I kept the copy deliberately: lifting a provider-neutral "polled app-log owner" into capture-kit would 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 in main, if maintainers prefer it that way.

Validation

  • Local gates: 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, and pnpm check:affected --base origin/main --run are green at this head. Package boundary and eager-closure gates were extended with rows for the new package.
  • Unit coverage: 92 tests in 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 doublespeed profile generation (iOS-only, rejects --platform android), help topics, and snapshot provenance.
  • Live device evidence at commit 8dad6500 against 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 --relaunchsnapshot -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.doublespeedSimulatorId returned, simulator ended, 66 billable seconds) → disconnect. The session was closed and released; no simulators remained allocated afterwards.
  • Not exercised live: app-log reattach after a daemon restart (covered by unit tests through the reconnect seam).

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.

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>
@thymikee

Copy link
Copy Markdown
Member

Not ready at 8dad6500a3.

  • P1 — prevent paid simulator leaks during allocation failure. DoublespeedApiClient.createSimulator() creates the simulator and then waits for readiness, while DoublespeedRuntime.allocate() only enters its rollback block after createSimulator() returns. A failed status, timeout, or abort during readiness therefore loses the simulator id and never issues DELETE. Split creation from readiness or make the client own rollback/finally. Plant failed-status, timeout, and cancellation tests proving DELETE occurs and the primary error remains authoritative.
  • P1 — do not advertise semantics the provider does not implement. Facts mark snapshot custom actions available, but ios.ts ignores SnapshotOptions; double-tap is two independent HTTPS taps, and fill taps then types without clearing. Mark these unsupported unless Doublespeed exposes owning primitives, or implement the actual semantics with planted route tests and exact-head live evidence.
  • P2 — stream app uploads. uploadAsset() readFiles the entire zipped .app before PUT, doubling memory for archives that can be hundreds of MB. Stream it with bounded cancellation and prove abort/large-body behavior.
  • Design/size — materially reduce the duplicated provider stack before landing. This is ~3,081 net production lines and explicitly mirrors ~590 Limrun lines across nine modules. This PR itself creates the second consumer, so ADR-0019s condition for extracting honest shared ownership is met now; a later cleanup is not a sufficient smaller-design rejection. Extract focused provider-neutral session/app-log machinery into its owning package (or otherwise shrink this slice), itemize justified residual growth, and remove implementation/test-tour comments that narrate the code or justify workarounds.
  • Evidence — complete exact-head gates and failure-path proof. GitHub currently has no check rollup. The happy-path device run is useful, but it does not cover allocation cleanup failures/cancellation, durable log reattach after daemon restart, or the claimed custom-action/fill/double-tap semantics. Add the smallest practical exact-head evidence for those paths.

@thymikee

Copy link
Copy Markdown
Member

Current exact head 8dad650 is unchanged and now CONFLICTING with current main. The direct Doublespeed route and prior live evidence are worth preserving, but confirmed blockers remain: allocation failures before runtime.ts:193 bypass DELETE cleanup and can leak paid simulators; advertised snapshot options are ignored, doubleTap is two independent taps, and fill does not clear; uploads buffer the full archive; and ~3,081 net production lines duplicate Limrun seams instead of extracting the now-proven provider-neutral owner. With nine current-main conflicts, no linked issue, and old-base CI, do not rebase this 5k-line single commit wholesale. Build a fresh current-main stack: shared provider-neutral session/app-log owner, rollback-safe streamed Doublespeed lifecycle, then only accurately supported capabilities with exact failure-path evidence. Keep this PR as reference and close it once replacement exists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants