Skip to content

feat(restapi): full CLI-parity endpoints (Waves 1-4: diagnostics, a11y, fsync, webinspector, UI automation, streams, codesigning) + uidriver - #821

Open
danielpaulus wants to merge 33 commits into
mainfrom
feature/restapi-complete
Open

feat(restapi): full CLI-parity endpoints (Waves 1-4: diagnostics, a11y, fsync, webinspector, UI automation, streams, codesigning) + uidriver#821
danielpaulus wants to merge 33 commits into
mainfrom
feature/restapi-complete

Conversation

@danielpaulus

Copy link
Copy Markdown
Owner

Wave 1: CLI-parity REST endpoints + ios/uidriver

This integrates the Wave-1 REST parity branches and the reusable UI-driver
library onto a single branch so Wave 2 can build on top of it. It is stacked
on
#817 (feature/restapi-parity, the base) and #820 (refactor/uidriver-extract)
and should be merged after both of those land.

Roughly 22 new REST endpoints are added, grouped below.

Diagnostics / network (w1a)

  • GET /device/:udid/diskspace — disk space usage
  • GET /device/:udid/ip — device IP address
  • GET /device/:udid/rsd — RSD info (iOS 17+ tunnel required)
  • GET /device/:udid/battery/registry — raw battery IORegistry values

Accessibility / location (w1b)

  • GET/PUT /device/:udid/voiceover — read / toggle VoiceOver
  • GET/PUT /device/:udid/zoom — read / toggle Zoom
  • POST /device/:udid/ax/audit — run an accessibility audit
  • GET /device/:udid/ax — accessibility inspector snapshot
  • PUT /device/:udid/setlocation/gpx — simulate location from an uploaded GPX file

Filesystem / provisioning (w1c)

  • GET /device/:udid/fsync/ls — list a directory (AFC)
  • GET /device/:udid/fsync/tree — recursive directory tree
  • GET /device/:udid/fsync/pull — download a single file
  • POST /device/:udid/fsync/push — upload a file
  • DELETE /device/:udid/fsync/rm — remove a path
  • POST /device/:udid/fsync/mkdir — create a directory
  • GET /device/:udid/cloudconfig — read the installed cloud configuration
  • GET /prepare/skip-options — supported Setup Assistant skip options (host-scoped)

WebInspector (w1d)

  • GET /device/:udid/webinspector/pages — list inspectable pages
  • POST /device/:udid/webinspector/launch — launch a URL / bundle
  • POST /device/:udid/webinspector/eval — evaluate JavaScript in a page

ios/uidriver extraction (#820)

Extracts the reusable UI-automation logic out of cmd_ui.go into a new
ios/uidriver package (with unit tests) and rewrites cmd_ui.go to consume it.
This is a prerequisite for the Wave 2 UI-automation REST endpoints.


This is Wave 1 of the feature-complete effort. Waves 2-4 to follow:
UI automation, streams, and codesigning.

Merge conflicts during integration were confined to restapi/api/routes.go
(route-registration calls) and restapi/api/errors.go (error sentinels); both
are additive and were resolved by keeping the union of all waves.

🤖 Generated with Claude Code
https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk

danielpaulus and others added 30 commits August 10, 2026 09:58
…ts, no-panic marshal)

Foundational hardening for running the REST API in production. No endpoint
behavior changes; the auth model from #792 (GO_IOS_API_KEY / --disable-auth)
is preserved.

- Serve via an explicit http.Server with graceful shutdown on SIGINT/SIGTERM
  (drains in-flight requests, 10s timeout).
- Bound abuse without breaking streams: ReadHeaderTimeout, IdleTimeout,
  MaxHeaderBytes. Deliberately no WriteTimeout so /syslog,/listen,/ostrace,
  /notifications can stream indefinitely.
- Optional TLS via --tls-cert/--tls-key; configurable bind via --addr
  (default :8080). Flags parsed in the same tolerant FlagSet as --disable-auth.
- Unauthenticated /healthz and /readyz probes outside /api/v1.
- Gate the swagger UI behind auth (served under /api/v1) when a token is set.
- MustMarshal no longer panics on an unmarshalable value — it returns a JSON
  error envelope, so a stream/handler can't be crashed by it.
- Add RespondError helper for a consistent {"error":...} envelope.

Tests: MustMarshal no-panic + valid-unchanged, health endpoints, RespondError,
parseServerConfig (defaults, flags, tolerates unknown args). go build/vet/test
./restapi/... green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
…s, mobilegestalt, ps, lockdown)

First batch of CLI-parity endpoints, all read-only, each mirroring the exact
go-ios library call the corresponding `ios` CLI command uses:

- GET /device/:udid/devicename    -> ios.GetValues (ios devicename)
- GET /device/:udid/date          -> ios.GetValues (ios date)
- GET /device/:udid/battery       -> ios.GetBatteryDiagnostics (ios batterycheck)
- GET /device/:udid/diagnostics   -> diagnostics.AllValues (ios diagnostics list)
- GET /device/:udid/mobilegestalt -> diagnostics.MobileGestaltQuery (ios mobilegestalt), keys via ?key=
- GET /device/:udid/processes     -> instruments.ProcessList (ios ps), ?apps=true filters apps
- GET /device/:udid/lockdown      -> ios.GetValues (ios lockdown get)

Handlers surface library errors as {"error":...} via RespondError (no discarded
errors, no panics). Wired through registerDeviceInfoRoutes in routes.go.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
…e, lang, memlimitoff)

Mirrors the ios CLI: diagnostics.Reboot/Shutdown, mcinstall.Erase (gated by
?confirm=true), amfi.EnableDeveloperMode + imagemounter.IsDevModeEnabled +
amfi.RevealDevMode, ios.Get/SetLanguage, and instruments ProcessControl
DisableMemoryLimit. Errors surfaced via RespondError; no panics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
…h, ios crash ls/rm)

Uses the iOS 17+ file service and streams pull/push through the HTTP body, so
there is no caller-supplied host path and no host-side traversal. Domains:
app|app-group|crash|temp. Crash reports: list + remove (crash-log downloads are
available via /files?domain=crash).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
- GET/PUT /wallpaper: springboard GetHomeScreenWallpaperPNG (image/png); set via
  multipart (image+p12 supervisor identity+screen) -> mcinstall.SetWallpaperSupervised.
- GET/PUT /icon-layout: springboard Get/SetIconLayout.
- GET/PUT /pasteboard: pasteboard Get/SetText (iOS 17+).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
…age list/unmount)

- POST /profiles: mcinstall AddProfile / AddProfileSupervised (multipart p12); DELETE /profiles/:name.
- GET /image/list: mounted image signatures (hex); DELETE /image: imagemounter.UnmountImage.
GET /profiles and GET/PUT /image already existed; only the missing verbs added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
- GET/PUT /assistivetouch: ios.Get/SetAssistiveTouch.
- GET/PUT /timeformat: ios.Get/SetUses24HourClock.
- PUT/DELETE /wifi: mcinstall.PrepareWifi/RemoveWifi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
GET /sysmontap streams instruments Sysmontap CPU-usage samples (matches the
existing syslog/listen streaming pattern). pcap is deferred until ios/pcap
exposes a packet-callback streaming API (it currently writes a local file).
…, clear-passcode, clear-screen-time-password)

All under /device/:udid/mdm, POST multipart with a p12 supervisor identity +
password (escalated mcinstall session via conn.Escalate). Credentials stay in
memory, never logged or persisted. clear-passcode also takes a base64 token.
…streamed logs

Long-running device operations now run as background jobs:
- POST /jobs/runtest, /jobs/runwda, /jobs/forward -> 202 + job id
- GET  /jobs (per-device), GET /jobs/:id (status), DELETE /jobs/:id (stop)
- GET  /jobs/:id/logs streams that job's isolated log (history + live tail)

Each job captures its output on a dedicated jobLog sink (io.Writer wired into the
testmanagerd TestListener), so concurrent jobs never interleave. Lifecycle events
are logged via ios/golog with module=go-ios/restapi + udid + job attrs. Terminal
state is immutable, so stopping a job isn't relabeled as a failure when its
context-cancelled goroutine returns. In-memory job manager + jobLog are unit-tested
(incl. -race): lifecycle, stop-is-terminal, per-device isolation, log stream/close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
PUT /httpproxy (supervised, multipart host/port/p12/user/pass/password) ->
mcinstall.SetHttpProxy; DELETE /httpproxy -> mcinstall.RemoveProxy.
Agent-level, not device-scoped, so they live at /api/v1 (behind auth):
- GET    /tunnels               list running tunnels
- DELETE /tunnels/:udid         stop a device tunnel
- POST   /tunnels/:udid/refresh refresh a device tunnel
- POST   /tunnel-agent/shutdown stop the tunnel agent
They query the running agent via ios.HttpApiHost/HttpApiPort. 'tunnel start' is
not exposed (privileged long-running daemon). Added a route-registration smoke
test that builds the full tree so gin route conflicts fail loudly.
httptest-based tests exercising the request-validation branches (which run before
any device I/O): missing/invalid params across files, mobilegestalt, wifi, mdm,
crashes, devmode, and the job endpoints (erase confirm-gate, missing bundle/ports,
job-not-found 404). Closes the biggest coverage gap for the parity endpoints.
Add RateLimitUDID: a token-bucket (golang.org/x/time/rate) gin middleware keyed
by device UDID, applied across the /device/:udid group. Requests over the limit
get 429; each UDID has its own bucket so devices don't throttle each other.
Configurable via --rate-limit (req/s, default 20) and --rate-burst (default 40);
0 disables. Uses the atomic sync.Map LoadOrStore pattern (no create race).

Tests (device-free, -race): burst-then-429, disabled-when-zero, per-device
isolation, and a concurrent-hammer test asserting the shared bucket isn't
exceeded under load.
Adversarial review follow-ups on the 56-endpoint REST parity work:

Security/correctness fixes:
- Bound in-memory uploads: readFormFile and the raw-body reads in
  SetPasteboard and AddProfile now go through readAllLimited (256 MiB cap)
  so an authenticated client can't OOM the daemon with an oversized
  multipart file or body. PushFile already streams (Content-Length gated)
  and is unaffected.
- Fix a lost-line gap in GET /jobs/:id/logs: snapshot() then subscribe()
  raced, dropping any line written in between. Added jobLog.
  snapshotAndSubscribe() which takes the backlog and the live subscription
  under one lock.
- Bound the process-wide job registry: DELETE /jobs/:id on an already
  terminal job now purges it (jobManager.remove, terminal-only) so finished
  jobs' buffered logs don't accumulate forever. Running jobs are still
  stopped, never silently dropped.

Tests (httptest + in-context device):
- Auth coverage: BearerAuth accept/reject, and a tree-walk asserting all
  80 registered /api/v1 routes return 401 unauthenticated.
- Files: Content-Disposition base-name sanitisation (traversal-y remote
  can't inject a host path), ls/pull/push validation, push 411 without
  Content-Length.
- Upload limits: readAllLimited boundaries + oversized pasteboard body.
- Proxy/MDM multipart validation.
- Jobs: full HTTP lifecycle (create/list/get/stop/delete), per-device
  isolation (cross-udid GET/DELETE 404 and no stop), remove-terminal-only,
  atomic snapshot+subscribe.

go build/vet/test ./restapi/... green incl. -race; gofmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Make the REST daemon match the TypeSpec-authored OpenAPI contract the
official SDKs are generated from.

Real SSE framing (event:/data:/blank-line) on all six streaming
endpoints, with a periodic heartbeat on idle, replacing the previous
NDJSON/concatenated-JSON writes:
  - /syslog       -> event "syslog"       (SyslogMessage)
  - /notifications-> event "appstate"     (AppStateNotification)
  - /ostrace      -> event "ostrace"      (OsTraceEntry)
  - /listen       -> event "attachdetach" (AttachDetachEvent)
  - /sysmontap    -> event "sample"       (CpuUsageSample)
  - /jobs/{id}/logs-> event "log"         (JobLogLine)
  - all           -> event "heartbeat"    ({}) on idle

Payload models use the spec's camelCase field names; a shared streamSSE
helper drives frames + heartbeats and flushes after each write.

Other spec conformance:
  - setlocation: longtitude -> longitude (query param, checks, messages,
    swagger annotations)
  - screenshot: content-type image/png (was application/octet-stream)
  - streaming error paths now use the GenericResponse envelope

Device-free unit tests cover the SSE framing + heartbeats, payload
mappers, longitude, and the removed misspelled param.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Add device diagnostics and network endpoints under /device/:udid:
- GET /diskspace       -> afc.(*Client).DeviceInfo (filesystem info)
- GET /ip              -> pcap.FindIp (MAC/IPv4/IPv6)
- GET /rsd             -> device.Rsd.GetServices (RSD service list; 400 if no tunnel)
- GET /battery/registry-> diagnostics.(*Connection).Battery (IORegistry stats)

Extend the existing GET /lockdown handler to accept an optional ?domain=
query param, returning domain-scoped values via GetValueForDomain.

Handlers live in a new diagnostics_net_endpoints.go with device-free unit
tests for the RSD capability/error paths and route registration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Expose the non-interactive `ios webinspector` operations over the REST API
under /device/:udid/webinspector:

- GET  /pages  -> list inspectable pages (client.ListPages)
- POST /launch -> open a URL via a remote automation session
  (OpenApp + AutomationSession + Start + Navigate)
- POST /eval   -> evaluate JS in a page (client.Evaluate)

The interactive commands (js-shell, cdp) are intentionally not exposed.
"Web Inspector / Remote Automation not enabled" conditions map to 424;
missing url/script and a non-existent page map to 4xx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Extract the private WDA/DeviceKit UI-automation HTTP client that lived in
cmd_ui.go into a new, exported ios/uidriver package so the CLI and the
upcoming REST ui endpoints can share one driver.

uidriver.Driver is constructed against a backend base URL (the forwarded
WDA :8100 / DeviceKit :12004 address) and exposes Tap/Swipe/LongPress/
Type/PressButton/Screenshot/Source/WindowSize/Orientation/SetOrientation/
AppLaunch/AppTerminate/AppForeground/Status/API/Stream. Methods return
values and errors instead of calling os.Exit, so the package is safe to
embed. Request/response types are exported and JSON-tagged.

cmd_ui.go keeps arg parsing, backend resolution (wda/devicekit/auto) and
output formatting, delegating all HTTP work to the driver. CLI behavior is
unchanged. Adds ios/uidriver unit tests driving the client against an
httptest backend (methods/paths/bodies for every action plus the chunked
streaming helper).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Adds the AFC-based (com.apple.afc) filesystem surface under
/device/:udid/fsync (ls, tree, pull, push, rm, mkdir), scoped to the
media directory or, with ?bundleID=, to an app data container via
house_arrest -- mirroring 'ios fsync'. Distinct from the iOS 17+
fileservice /files surface.

Also adds provisioning parity: GET /device/:udid/cloudconfig
(mcinstall GetCloudConfiguration) and the device-free fleet route
GET /prepare/skip-options (mcinstall GetAllSetupSkipOptions).

Caller-supplied device paths are validated by safeDevicePath, which
rejects any '..' element so a request cannot traverse out of the AFC
root; pull additionally relies on the afc package's own entry-name
filtering. Uploads stream to the device and are bounded by
maxUploadBytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Add device REST endpoints for accessibility + location parity:
- GET/PUT /device/:udid/voiceover  -> ios.GetVoiceOver / ios.SetVoiceOver
- GET/PUT /device/:udid/zoom       -> ios.GetZoomTouch / ios.SetZoomTouch
- POST    /device/:udid/ax/audit   -> accessibility RunAudit (bounded timeout)
- GET     /device/:udid/ax         -> accessibility element snapshot (no listeners)
- PUT     /device/:udid/setlocation/gpx -> simlocation.SetLocationGPX (multipart)

Set handlers accept enabled via JSON body or query param. GPX upload is
size-bounded via readFormFile, written to a temp file and cleaned up.
Device-free tests cover validation/error paths, enabled parsing precedence,
audit timeout validation, and GPX multipart parsing/temp handling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
…ure/restapi-complete

# Conflicts:
#	restapi/api/errors.go
…ture/restapi-complete

# Conflicts:
#	restapi/api/errors.go
#	restapi/api/routes.go
…nto feature/restapi-complete

# Conflicts:
#	restapi/api/routes.go
Add /api/v1/device/:udid/ui endpoints that drive on-device UI automation
through the ios/uidriver package against a running, forwarded WebDriverAgent
(or DeviceKit) backend.

Endpoints (all under /device/:udid/ui):
  POST tap, swipe, longpress, type, button
  GET  screenshot (image/png), source, size, orientation, status
  PUT  orientation
  POST app/launch, app/terminate, app/foreground
  POST api (raw uidriver.API passthrough)

Backend is selected per request via ?backend=wda|devicekit and addressed via
?wdaUrl=<url> (defaults 127.0.0.1:8100 / :12004), with an optional ?timeout.
A fresh uidriver.Driver is built per request. uidriver errors are mapped:
*HTTPError and transport/unreachable failures -> 502, unsupported actions
(button/foreground/stream) -> 501, bad input -> 400.

Tests stand up an httptest server mimicking WDA and assert each endpoint
forwards the correct method/path/body and returns the mapped response,
including unreachable-backend -> 502 and bad-input -> 400. Fully device-free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
…terface

Add host-local /api/v1/sign/{certificate,provision,app} and prepare routes
(/prepare/create-cert host, /device/:udid/prepare) to the daemon. Endpoints
depend only on a new Signer interface (restapi/api/signing_adapter.go); the
default signingAdapter wraps ios/signing and is swappable for the future
in-repo ios/codesign impl with no endpoint/test changes.

Uploads are bounded by maxUploadBytes and written to a per-request temp dir
that is always cleaned up; generated P12/profile artifacts are read then
removed from disk. No key bytes or passwords are ever logged.

Tests inject a mock Signer to verify multipart parsing, temp-dir write +
cleanup, correct Signer args, secret redaction, and artifact streaming —
fully device-free and impl-free — plus a compile-time Signer assertion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
@danielpaulus

Copy link
Copy Markdown
Owner Author

Merged Wave 2 (UI automation) + Wave 4 (codesigning)

feature/restapi-complete now includes two more waves on top of the Wave-1 CLI-parity set:

  • Wave 2 — UI automation (~15 endpoints): device-scoped /ui/* routes (registerUIRoutes) that proxy to a forwarded WDA/DeviceKit backend — tap, swipe, longpress, type, button, screenshot, source, window size, get/set orientation, status, app launch/terminate/foreground, and a raw /api passthrough. (ui_endpoints.go, ui_endpoints_test.go)
  • Wave 4 — codesigning (~5 endpoints, behind a Signer interface): host-local /sign/certificate, /sign/provision, /sign/app, /prepare/create-cert, and the device-scoped /device/:udid/prepare. Signing is abstracted behind a Signer interface (signing_adapter.go) so it's testable without real Apple credentials. (sign_endpoints.go, sign_endpoints_test.go)

Merge notes: both branches merged with no real conflicts — Wave 2 fast-forwarded, Wave 4 auto-merged (only additive registerRoutes insertions in routes.go). No sentinel-error name collisions: each wave namespaces its own sentinels (e.g. Wave 2 uses errMissingBundleIDUI to avoid clashing with the existing errMissingBundleID). All Wave-1 registrations remain intact.

Endpoint total on this branch: 121 (Wave-1 baseline 101 + 15 UI + 5 sign; counting distinct gin route registrations).

Verification: go build ./..., go vet ./restapi/api/..., go test ./restapi/api/..., go test -race ./restapi/api/..., the TestRegisterRoutes route-conflict test, and the full go test ./... suite all pass; gofmt clean on all merged files.

Still open: Wave 3 is not yet included, and final integration (docs/swag regeneration, cross-wave review) remains for the integrator.

Comment thread ios/uidriver/uidriver.go
req.Header.Set("Content-Type", "application/json")
}
golog.Debug("ui request", "module", logModule, "backend", string(d.backend), "method", method, "endpoint", endpoint, "udid", d.udid)
resp, err := d.httpClient.Do(req)
if path == "" {
return nil, nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if rmErr := os.Remove(path); rmErr != nil {
danielpaulus and others added 3 commits August 10, 2026 22:19
Refactor ios/pcap to a context-aware streaming core: new
Stream(ctx, device, io.Writer) writes a valid pcap stream (global header +
per-packet records) until ctx is canceled, then returns cleanly by closing
the device connection to unblock the read (no goroutine leak). The existing
Start(device) CLI entry point keeps its signature and is reimplemented on top
of Stream, so `ios pcap` and its caller are unchanged.

Add GET /device/:udid/pcap streaming the live capture as
application/vnd.tcpdump.pcap (chunked, flushed per write). Capture lifetime is
the request context plus an optional ?timeout= (seconds, default 60s, capped at
3600s); client disconnect or the deadline stops it. Registered in routes.go in
a delimited block.

Tests are device-free: ios/pcap capture is verified against a fake connection
feeding canned frames (valid pcap out, stop on cancel/deadline, error surfaced
when not canceled, single close); the endpoint covers timeout parse/validation
and route registration. Live capture bytes cannot be mocked at the endpoint
layer (pcap.Stream connects internally), so that behavior is covered in the
pcap package tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Add two streaming REST endpoints to the go-ios daemon:

- GET /device/:udid/ui/stream?codec=mjpeg|h264 proxies a forwarded
  WDA/DeviceKit UI video stream straight through to the client,
  preserving the backend's Content-Type and flushing frames until the
  client disconnects or the backend ends. Maps ErrStreamUnsupported->501
  and unreachable backends->502.
- GET /device/:udid/screenshot/stream serves a multipart/x-mixed-replace
  MJPEG stream of device screenshots via the instruments screenshot
  service, flushing each frame until the client disconnects.

uidriver: add StreamWithContentType (Stream now delegates to it) so the
proxy can forward the backend's Content-Type verbatim; existing Stream
signature and tests are unchanged.

instruments: extract the shared MJPEG core (pngToJPEG conversion and a
reusable, context-driven StreamJPEGFrames frame source) out of the
package-global screenshot loop. StartMJPEGStreamingServer and mjpegHandler
keep their prior behavior, now fanning a single StreamJPEGFrames source
out to registered consumers.

Tests are device-free: ui/stream is exercised against an httptest backend
(pipe-through, Content-Type preservation, codec/backend error mapping,
client-disconnect stop); the screenshot multipart framing core is tested
via an injected frame channel; pngToJPEG has round-trip coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
…tree-agent-a9c552bbe20d85f56

# Conflicts:
#	restapi/api/routes.go
@danielpaulus

Copy link
Copy Markdown
Owner Author

Wave 3 merged: streaming endpoints

Both Wave-3 streaming branches are now integrated into feature/restapi-complete:

  • feat/restapi-w3a-streams — live UI video + mjpeg screenshot streaming
    • GET /device/:udid/ui/stream — live UI video stream (?codec=mjpeg|h264; mjpeg = multipart/x-mixed-replace, h264 via the devicekit backend), proxied through a forwarded WDA/DeviceKit backend
    • GET /device/:udid/screenshot/stream — mjpeg screenshot stream
    • lib: ios/uidriver StreamWithContentType + ios/instruments/screenshot.go
  • feat/restapi-w3b-pcap — live packet capture
    • GET /device/:udid/pcap — streaming pcap capture
    • lib: ios/pcap/pcap.go streaming core

Conflicts: only the expected additive collision in restapi/api/routes.go (the w3a registerStreamRoutes block vs the w3b registerPcapRoutes block); resolved by union — both are registered. w3a fast-forwarded cleanly; the disjoint lib/endpoint files (stream_endpoints.go vs pcap_endpoints.go) merged without conflict.

Endpoint total: the /api/v1 route tree is now 125 registered routes (122 → 125, +3 for Wave 3). All prior wave registrations remain present (tunnel, provisioning, sign host/device, device-info, diagnostics/net, device-mgmt, files, media, config, settings, monitoring, mdm, jobs, proxy, accessibility, fsync, webinspector, UI, apps).

Verification: go build ./..., go vet ./restapi/api/..., go test ./restapi/api/... (incl. -race), the TestRegisterRoutesNoConflict route-conflict test, the lib suites (ios/pcap, ios/instruments, ios/uidriver), and the full go test ./... all pass; gofmt -l clean on the resolved files.

Parity status: the daemon is now at CLI parity except the documented NOT-REST exclusions, which are inherently interactive/stateful and unsuitable for stateless REST:

  • interactive debug (ios debug — LLDB session)
  • ios dproxy (debug proxy)
  • ios tunnel start
  • ios webinspector js-shell / ios webinspector cdp
  • ios readpair

Comment thread ios/uidriver/uidriver.go
return nil, "", fmt.Errorf("uidriver: failed creating stream request: %w", err)
}
golog.Info("opening ui stream", "module", logModule, "backend", string(d.backend), "url", rawURL, "udid", d.udid)
resp, err := d.httpClient.Do(req)
}

func (f flushWriter) Write(p []byte) (int, error) {
n, err := f.w.Write(p)
@danielpaulus danielpaulus changed the title feat(restapi): Wave 1 CLI-parity endpoints + uidriver feat(restapi): full CLI-parity endpoints (Waves 1-4: diagnostics, a11y, fsync, webinspector, UI automation, streams, codesigning) + uidriver Aug 11, 2026
danielpaulus added a commit that referenced this pull request Aug 11, 2026
Integrate the five per-language examples suites (typescript, python, java,
csharp, mcp) and tie them together:

- sdks/docs/EXAMPLES.md: top-level overview (docs + pre-release smoke test),
  per-language index, shared GO_IOS_BASE_URL/GO_IOS_API_KEY/GO_IOS_UDID/RUN_UI
  convention, how to run them all. Linked from sdks/README.md. Documents the
  canonical daemon port (8080, matching the daemon's --addr default) and the
  SDK-library-vs-daemon default discrepancy (libraries default to :60105 from
  the spec servers URL) for a follow-up reconcile.
- sdks.yml: add the device-free MCP list-tools smoke check (npm run examples)
  to the mcp job so a broken MCP server fails CI.
- verify-sdk-examples.yml: new dispatch-only, farm-gated workflow that runs
  every SDK's example runner against a live go-ios REST daemon + device. Inert
  until the full REST daemon lands on main (#817/#821); the daemon-start step
  is a documented placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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