Skip to content

feat: orchestrator core extraction + API hardening - #35

Merged
espetro merged 56 commits into
developfrom
feat/orchestrator-core
Sep 13, 2026
Merged

espetro merged 56 commits into
developfrom
feat/orchestrator-core

Conversation

@espetro

@espetro espetro commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Closes #31. Plan: .agents/plans/2026-09-09-orchestrator-core.md (repo). Decision record: .agents/docs/ARCHITECTURE.md.

What

Three-layer pivot: MCP transport (pkg/mcp) <-> Go orchestrator core (pkg/orchestrator) <-> simulator adapters (pkg/contract). internal/core deleted.

  • pkg/contract: CapabilitySet (uint32 bitmask) + required Capabilities() on Platform; As[T] escape hatch. Optimizer stays the optional interface, advertised via caps.
  • pkg/orchestrator (new, public): concrete Orchestrator struct, fallible functional options (WithLogger/WithPlatform/WithController/WithWipePolicy), loud duplicate-registration errors, ShutdownAll(ctx). Imports only pkg/contract + stdlib.
  • Idempotent actions: boot on a running device succeeds after profile reconciliation; stop on stopped is a no-op success; wipe twice converges.
  • Wipe reconciliation: Wipe returns the device to its configured baseline (erase + re-apply slim profile when on_boot set) via the iOS adapter's new ReconcileProfile; Boot is the universal convergence point.
  • Eventually-consistent reads: list_devices skips a failing platform (warn log) instead of failing the whole call; errors only when all platforms fail.
  • Unified wiring: single bootstrap.BuildOrchestrator for serve/mcp/service modes; pkg/mcp is a thin transport (tool names + schemas byte-identical, verified via live tools/list).
  • Guardrails: conformance.RunSuite wired into ios (passes vs real simulators) and android (skips w/o SDK); go-apidiff CI on pkg/contract + pkg/orchestrator; bench-slim orchestrator smoke behind MCPSIM_BENCH_TARGET.
  • Docs: user docs in docs/ (light architecture overview + embed example, new agent-setup.md, agentic README pass); contributor decision record in .agents/docs/ARCHITECTURE.md (unhosted). CHANGELOG Unreleased sections deduplicated; version bumped to 0.3.0-dev.

Intended behavior deltas

Tool Before After
boot_device (running) already_running error success, reconciles profile, returns current device
stop_device (stopped) not_running error no-op success
wipe_device (slim + on_boot) erase to stock erase + re-slim to configured baseline
list_devices (one platform down) total failure partial results + warn

Validation

  • task validate green (typecheck + lint + test), task build ok
  • Live smoke: serve mode, healthz ok, MCP handshake + tools/list = 10 tools unchanged
  • iOS conformance suite passes against real simulators (~32s); Android skips without SDK; fake-platform unit tests keep non-macOS CI green

Add CapabilitySet (uint32 bitmask) with named capability constants and
Capabilities() as a required Platform method. Adapters advertise base
lifecycle caps; CapOptimize/CapMeasure only when contract.Optimizer is
implemented. As[T] generic escape hatch documented in adding-platform.md.
Concrete Orchestrator struct with fallible functional options, loud
duplicate-registration errors, capability-driven optimizer dispatch,
eventually-consistent List (partial platform failure degrades, not
fails) and idempotent lifecycle actions. pkg/orchestrator imports only
pkg/contract + stdlib. internal/core deleted.
Single bootstrap.BuildOrchestrator used by serve, mcp, and service
modes. pkg/mcp becomes a thin transport over the orchestrator (tool
names and schemas unchanged). iOS Platform gains ReconcileProfile so
the orchestrator's profile policy can converge devices after wipe and
boot.
conformance.RunSuite drives an adapter through the orchestrator (list,
state, stop idempotency, capability honesty); wired into ios and
android test suites behind SDK detection guards. go-apidiff CI step
watches pkg/contract and pkg/orchestrator public API. bench-slim gains
an orchestrator-level wipe convergence smoke.
User docs (docs/, hosted): light architecture overview with embed
example, agent-setup guide, agentic README pass. Contributor record in
.agents/docs/ARCHITECTURE.md (unhosted) per the docs placement rule.
CHANGELOG entry for the orchestrator extraction.
The simslim branch left two '## [Unreleased]' headers; merge them into
one section with proper Added/Changed grouping.
Add aosp_atd/google_atd image tag support to the Android adapter:
config fields (image_tag, api, abi, ram_size, heap_size, auto_provision)
with MCPSIM_ANDROID_* env equivalents, config.ini tag detection, ATD
launch flags (-no-window -no-audio -no-boot-anim -no-snapshot
-gpu swiftshader_indirect -memory), boot-time AVD auto provisioning via
sdkmanager/avdmanager, and atd/est_ram_mb annotations on Device.

Issue #34 (M2).
Add platforms/redroid implementing contract.Platform over the Docker CLI
(docker run redroid/redroid:<ver>_64only, adb connect to the mapped
port). Build-tag guarded to linux so macOS builds are unaffected;
registration no-ops unless MCPSIM_REDROID_ENABLED is set. Wipe equals
container removal (fresh container, fresh data). Documented as WIP and
untested, including host kernel binder requirements and the macOS
non-viability finding.

Issue #34 (M2).
Add a stream_info tool returning scrcpy over TCP guidance for running
Android devices (adb tcpip 5555, adb connect, scrcpy --tcpip) and an
explicit unsupported response for non running devices. Document the
screencap fallback for ATD images and the agent-first rationale for not
embedding streaming.

Issue #34 (M2).
The MCP server dispatches tool calls concurrently, and get_state or
await_ready racing boot_device could mutate the unsynchronized
avdPortMap from two goroutines, crashing the process with a fatal
concurrent map writes error (caught by the live ATD boot smoke).
Serialize all reads and writes with a mutex.

Issue #34 (M2).
boot_device spawned the emulator with exec.CommandContext bound to the
HTTP request context, so the emulator was killed as soon as the boot
response was flushed and the client disconnected. The AVD boot smoke on
a remote host caught it: qemu died right after every boot_device call.
Spawn with a detached context (context.WithoutCancel) in Start and in
the Wipe restart so the emulator outlives the request.

Also stop surfacing raw adb get-state errors from State: a cold adb
server or unregistered serial is a stopped device, not a hard error;
the old path aborted Boot before Start could run and leaked bare
'exit status 1' messages to tool callers.
get_state never reported the optimizer block: contract.As[Optimizer]
asserts the registered Platform itself, but only the internal slimmer
type implemented the interface, so the assertion always failed and
OptimizerState silently returned nil. Delegate Optimize, Restore,
OptimizeStatus and Measure to the slimmer (unsupported error when slim
is disabled) so capability gating and the assertion agree.

Caught by the mac mini POC after enabling platforms.ios.slim.
@espetro
espetro merged commit e22ab04 into develop Sep 13, 2026
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.

1 participant