## Summary
Sets up the foundation for adding Android support without changing any
user-visible iOS behavior. iOS works exactly as before; Android handlers
are stubs that throw a clear \"not yet implemented\" message until
they're filled in.
### New primitives
- `Platform`, `DeviceKind`, `DeviceInfo`, `ToolCapability`,
`ToolDependency` (in `@argent/registry`)
- `classifyDevice(udid)` + `resolveDevice(udid)` (UUID shape → iOS, else
Android)
- `assertSupported(toolId, capability, device)` — throws
`UnsupportedOperationError`
- `ensureDep(s)` — host-binary preflight with install hints
- `dispatchByPlatform({ toolId, capability, ios, android })` — one-line
cross-platform `execute()`
### HTTP gating
`/tools/:name` now runs:
- Host-binary preflight (`requires`) → 424 with install hint
- Capability gate (`capability` + `udid`) → 400 with reason
### Per-tool directory layout
Every cross-platform tool now lives in its own directory:
\`\`\`
tools/gesture-tap/
index.ts ← ToolDefinition, capability, dispatchByPlatform
platforms/
ios.ts ← iOS impl (existing logic)
android.ts ← stub: throws \"not yet implemented\"
\`\`\`
Refactored 17 tools this way: gesture-{tap,swipe,custom,pinch,rotate},
screenshot, button, paste, keyboard, launch-app, restart-app,
reinstall-app, open-url, rotate, run-sequence, describe (with shared
contract + iOS adapters under platforms/).
### Tool call flow (iOS, today)
1. Agent calls \`gesture-tap { udid: \"<uuid>\", x, y }\`
2. HTTP zod-parses, then host-binary preflight, then capability gate
(passes — capability has \`apple\`)
3. Tool's \`execute()\` (built by \`dispatchByPlatform\`):
- \`resolveDevice(udid)\` → \`{ platform: \"ios\", kind: \"simulator\",
id }\`
- \`assertSupported\` → passes
- branches to \`tapIos(services, params, device)\`
4. iOS impl uses \`SimulatorServerApi\` exactly as before.
### Tool call flow (Android, future)
Same flow; the Android branch in \`dispatchByPlatform\` runs. Filling in
Android = three steps:
1. Implement the function in \`tools/<tool>/platforms/android.ts\`
2. Add \`android: { emulator: true, device: true, unknown: true }\` to
the tool's capability
3. Make \`services()\` platform-aware where the iOS service URN doesn't
apply (e.g. \`launch-app\` declares \`nativeDevtools\` for iOS only)
## Test plan
- [x] All 395 existing tests pass; 9 new tests for `device-info` +
`capability`
- [x] TypeScript clean across `@argent/registry` and
`@argent/tool-server`
- [ ] Manual smoke-test on a real iOS simulator (gesture-tap,
screenshot, describe, launch-app)
## Deferred to follow-ups
- **Per-tool capability declarations on iOS-only tools**
(`native-devtools/*`, `profiler/ios-profiler/*`, `network/*`,
`simulator/*`). They still work; the HTTP capability gate is a no-op
when no declaration exists. Adding them is mechanical and doesn't affect
iOS behavior.
- **`list-simulators` / `boot-simulator` rename to `list-devices` /
`boot-device`** — that's a behavior change, paired with the Android
impls.
- **Platform-aware `services()`** for tools whose iOS service has no
Android equivalent (e.g. `nativeDevtools` in `launch-app` /
`restart-app`). Needed when filling in Android impls.