Skip to content

agent-device integration: bind the HTTP control plane into agent-device as a remote lease provider #66

Description

@V3RON

agent-device integration: bind the HTTP control plane into agent-device as a remote lease provider

Depends on #65 (the HTTP control plane). This issue is the plan for making a remote agent's agent-device CLI lease devices from a simlock host and drive them over the wire — simlock as the single authority over devices, agent-device as the hands.

Verified context (agent-device 0.20.6 source, callstack/agent-device)

  • agent-device's daemon has a first-class provider adapter contract, ProviderDeviceRuntime (packages/contracts): provider id, leaseLifecycle {allocate, heartbeat, release}, deviceInventoryProvider, ownsDevice, getInteractor, optional installApp/installInstallablePath/configurePortReverse/recoverExpiredLease. Lease commands dispatch on the request's leaseProvider string.
  • The provider registry is compiled in (src/provider-device-runtimes.ts): WebDriver providers (BrowserStack, AWS Device Farm) always; Limrun dynamically imported iff LIMRUN_API_KEY is set. No config/env hook loads an external runtime module.
  • A --remote-config profile already carries leaseProvider as a first-class field, and the "provider not available" gate only rejects built-in required providers that failed to load — so an external provider id flows through without touching the closed ConnectProvider unions, connect adapters, or help surface.
  • agent-device proxy (host-side) already covers the data plane: remote install / install-from-source, snapshots, artifacts, tunneling via cloudflared/ngrok — but with one shared token for the whole daemon and its own first-come-first-served 5-minute-inactivity device leases (no queue, no provisioning, no reclaim).

Hook mapping

agent-device provider hook simlock HTTP API (#65)
leaseLifecycle.allocate POST /v1/lease-requests → poll/SSE until granted
leaseLifecycle.heartbeat POST /v1/leases/{id}/renew (reads notices for device-health facts)
leaseLifecycle.release DELETE /v1/leases/{id}
deviceInventoryProvider GET /v1/catalog
recoverExpiredLease GET /v1/leases/{id}404 = gone, clean up locally

Simlock's detached lease mode (token + TTL + renew) maps 1:1 onto agent-device's lease model; held mode is not used remotely.

Phased plan

Phase 0 — interim, zero upstream changes. Operator runs simlock (HTTP enabled) and agent-device proxy on the host, tunnel in front of both. The remote agent leases out-of-band (POST /v1/lease-requests via curl/script), then agent-device connect proxy and opens the granted udid. Two lease clocks, honor-system scoping via the shared proxy token — but a full working loop to dogfood.

Phase 1 — upstream PR: external providers via config. Generalize the Limrun loading path: a ~/.agent-device/config.json key (e.g. externalProviders: [{provider, module, options}]) that dynamic-imports a module exporting createProviderRuntime(options, context), validates the ProviderDeviceRuntime shape, and merges it into the composition. Est. ~300–500 LOC + the trust-model conversation (absolute paths only, no auto-install, explicit opt-in). No connect <name> CLI surface needed — connect --remote-config with leaseProvider: "simlock" covers connection UX.

Phase 2 — the simlock provider module. Implements the hook mapping above against #65. dataPlane in the lease grant becomes a real connection bundle:

"dataPlane": {
  "kind": "agent-device-proxy",
  "baseUrl": "https://host.example/agent-device",
  "token": "<credential>",
  "device": { "platform": "ios", "udid": "ABCD-..." }
}

Phase 3 — scoped data-plane tokens. Second upstream change: the proxy honors per-lease credentials scoped to one device, minted at grant and revoked at release, turning "never touch someone else's leased device" from advisory into enforced.

Open questions (need agreement before Phase 1+)

  1. Data-plane scoping in the meantime: accept advisory scoping for v1 (bundle carries the shared proxy token; isolation is honor-system, as it is locally today) and document it loudly — or block on Phase 3?
  2. getInteractor feasibility (biggest technical unknown): can a provider runtime route interaction through agent-device's existing remote-daemon/proxy transport, or do provider runtimes assume they implement their own interactor (as Limrun does)? Needs a timeboxed spike with a stub provider before committing to the Phase 1 PR — the answer may flip the architecture from leaseProvider to "proxy flow with external admission".
  3. Device-lifecycle handshake: does agent-device open cleanly adopt a device simlock already booted? Must simlock force-close the agent-device session before reclaim/erase (almost certainly yes — erasing under a live XCTest runner is undefined)? How do simlock's health-monitor reboots interact with agent-device's iOS runner ownership (runner is already owned by another agent-device daemon is a documented failure mode)? Needs a written contract; known-pitfalls material.
  4. Contract stability + packaging: ProviderDeviceRuntime is an internal contract today — the Phase 1 PR conversation must include making it public/semver-stable. Where does the provider module live (@simlock/agent-device-provider? inside this repo?) and what is its compatibility matrix with agent-device versions?
  5. Identity/token UX at fleet scale: one token per agent session is the intended model — how are tokens provisioned to remote Claude Code sessions? Is one-lease-per-token still right when a requester legitimately wants two devices (app + companion)?
  6. Who supervises the host stack: does simlock spawn/supervise agent-device proxy and the tunnel (new process-supervision surface), or is that the operator's launchd config? Affects where dataPlane.baseUrl/token come from at grant time.
  7. Timing numbers: provider heartbeat cadence vs lease.detachedTtlMs (renew interval must clear a few missed beats), and the proxy's 5-minute inactivity lease vs simlock's lease TTL — simlock's lease must strictly outrank and outlive the proxy session.
  8. Documented tunnel path: pick one tested recommendation (Tailscale for persistent self-hosting, cloudflared for quick trials) rather than "any tunnel works".

Out of scope here

Everything the HTTP control plane itself needs — that's #65. Multi-host fleets (the per-lease baseUrl leaves room for a future broker). Live screen streaming for humans.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions