Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/android-desktop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Android emulator Desktop preview

Copse can connect to a running Android emulator from **Desktop**, alongside local
iOS Simulators and remote desktops. This first slice supports macOS hosts and the
emulator's local token-authenticated gRPC endpoint.

1. Start an AVD from Android Studio's embedded emulator, or launch it with the SDK
emulator's `-grpc <port> -grpc-use-token` options. Use the installed binary's
help to check supported flags. A headless emulator (`-no-window`) also works.
2. Enable the Desktop viewer in Copse Settings, open Desktop, and use **Refresh
desktop devices** if the emulator started after opening the panel.
3. Select the emulator and connect. It starts in view-only mode. **Control
emulator** enables touch, drags, keyboard input, and Back / Home / Apps buttons.
4. Disconnect to release the display and input streams. Copse leaves the emulator
and its apps running.

The connection is independent of Apple Development and does not require Xcode or
screen-recording permission. It discovers emulator process records from the
standard macOS registration directory. Tokens remain in the main process and are
sent only to the advertised port on `127.0.0.1`; Copse neither disables emulator
authentication nor accepts arbitrary gRPC addresses from renderer IPC.

An emulator configured exclusively for signed JWT authentication is shown with an
actionable connection error. JWT key registration is not implemented in this
slice. No emulator process or personal AVD is launched or modified automatically.

## Implementation and limits

- Uses the SDK's `android.emulation.control.EmulatorController` API, with a small
protobuf descriptor for display and input messages and the maintained Node gRPC
client. There is no VNC server or host-window capture helper.
- Streams PNG frames at the primary display's native dimensions. The canvas fits
them into the existing Desktop viewer. Rotation updates the canvas dimensions; touch coordinates are mapped back to the native digitizer orientation.
- Input uses one `streamInputEvent` connection: the emulator explicitly guarantees
event ordering on this RPC. Writes are paced by 16 ms for the UI-loop scheduling observed on emulator 33.1.24; pending pointer moves are coalesced so high-rate motion does not create a backlog. Independent `sendKey` calls can reorder rapid input,
even when the client waits for each call's response.
- Bounds frame size, queued input, initial-frame wait, and input writes. Closing
the viewer releases held touch/modifier state and closes both streams. Streams
are scoped to their owning renderer, with one connection per running emulator.
- Connected means display frames are arriving; it does not imply Android has
completed booting or an app has finished launching.

The first slice covers one pointer and common physical keyboard keys. Clipboard,
IME, multitouch, foldables/resizable displays, secondary displays, audio, remote
emulator transport, JWT, and Linux/Windows discovery need separate validation.
SDK installation, AVD creation/boot controls, Gradle build/test/install workflows,
and agent-visible Android tools are follow-up work, not part of this connection.

## Evidence

The [Android spike](spikes/android-emulator.md) records the original SDK/API
investigation and disposable test app. Integration tests run a real local gRPC
server to exercise credentials, ordered input, stream cancellation, ownership,
rotation dimensions, and reconnect. The focused Electron spec
`tests/e2e/simulator-desktop.e2e.ts` checks both platforms, explicit control,
navigation, refresh, and disconnect, and saves `android-desktop-live.png`.

Protocol reference:
[EmulatorController](https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-main/emulator/proto/emulator_controller.proto).

Real-device validation on emulator **33.1.24.0 / Android 13 / arm64**, with a disposable
AVD and the spike activity, confirmed a live 1080×1920 display inside Electron,
a panel tap changing the counter to `Taps: 1`, text `panel` arriving in order,
and disconnect. A landscape test confirmed 1920×1080 frames and a correctly
mapped tap after inverse rotation. See the [real panel capture](spikes/android-emulator/evidence/copse-live-panel.png).
72 changes: 64 additions & 8 deletions docs/plans/background-supervisor.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

Tracking: [#1081](https://github.com/copse-dev/agent-pane/issues/1081)

**Status: Active (P7 complete).** Design contract is on `develop` via [#1170](https://github.com/copse-dev/agent-pane/pull/1170).
P1 landed the Zod/JSON schema + pure load/reconcile helpers. P2 adds the durable
main-process store, lifecycle APIs, restart reconciliation, and one-shot scheduling
without registering a production consumer yet. Implementation PRs should link here
and keep long-horizon checklists (#558), dark-factory orchestration, A2A/remote
delegation (#1015), and `run_background` shell tasks as **consumers**, not alternate
supervisors.
**Status: Implemented through the #1081 supervisor contract.** Design contract originated in [#1170](https://github.com/copse-dev/agent-pane/pull/1170).
The transport-independent service owns persistence, scheduling, bounded execution,
recovery, and client operations. Long-horizon continuations, CI watches, automation
ticks, event preparation, and background processes are consumers. The phases below
record their delivery; campaign orchestration remains a separate follow-up. New
consumers must share this lifecycle instead of building alternate supervisors.

Parent investigation: [`grok-build-architecture-comparison.md`](grok-build-architecture-comparison.md).
Related foundations: [`long-horizon-tasks.md`](long-horizon-tasks.md),
Expand Down Expand Up @@ -261,7 +260,64 @@ dark-factory poller implementation, and changes to `run_background`.
app was closed is reconciled on reopen. Stale epochs and exhausted continuation budgets
block visibly instead of starting obsolete work.

### P6 — Campaigns + authenticated trigger adapters
### P9 — Recovery, bounded execution, and shared client operations

- [x] `TaskSupervisor` is transport independent: inject a store, clock, handler registry,
concurrency limits, and cancellation grace. Desktop and headless hosts share
`createSupervisedTaskClient(supervisor, owner)` for `list`, `get`, `cancel`, and
`resume`. Bind `owner` from the host's trusted project/thread context, never from
a model-supplied task record. The client cannot reassign or adopt work. Returned
summaries omit handler input and permission snapshots. The trusted desktop also
provides a project overview so orphaned tasks remain inspectable/cancellable.
- [x] Resume/retry is an explicit client action. Cancelled/completed tasks stay terminal;
failed/blocked tasks can begin a new bounded attempt cycle with an audit record.
Lost shell processes must be started as new commands, through the normal gate.
- [x] Cancellation publishes a terminal fence before process-exit/abort callbacks.
No handler starts after a cancellation that won while the start write was pending.
Handlers receive an `AbortSignal`; after the default five-second grace the
supervisor stops awaiting an uncooperative handler. Its real execution continues
to occupy a concurrency slot until it settles, and cannot be resumed concurrently.
JavaScript work cannot be forcibly killed; process consumers own OS termination.
- [x] `resourceBudget.maxDurationMs` bounds one execution attempt (up to the platform's
2,147,000,000 ms timer limit). Timeouts fail and abort the attempt; they are not
automatically retried because an uncooperative execution may still be active.
`resourceBudget.maxAttempts` can tighten the task's ordinary attempt cap.
- [x] Reported handler errors retry only with an explicit `retryPolicy`, using a persisted
`retryAt` deadline and capped exponential delay. The original event/cron trigger
survives retries. Long-horizon continuations use three attempts, starting at one
second with a thirty-second cap, and retain dispatcher operation-id deduplication.
- [x] Running work without an attachable process blocks on crash or clean shutdown by
default. Only consumers declaring `restartPolicy: 'retry'` replay automatically;
durable, idempotent event preparation opts in. Waiting/queued work rearms normally.
- [x] `reapproveOnWake` requires an explicit resume for one attempt. That consent is held
in memory, consumed before dispatch, and never survives restart or another cron
occurrence. It grants scheduling consent only: consumers still check the captured
execution identity and use the existing tool permission gate. Resume never refreshes
an expired snapshot or silently changes the workspace/policy binding. Long-task
snapshots expire 24 hours after the requested wake; changed/expired contexts require
scheduling new work through the consumer. Preparation-only handlers may prepare
a draft with `preparesOnly`, but cannot execute tools or dispatch a model turn.
Consumers with an existing approval model declare `reapprovesWake` and keep their
own gate authoritative: Apple operations validate their per-call/process-epoch
grant, which generic Resume never clears or renews.
- [x] One-shot overdue wakes run on admission/restart. Cron deadlines persist in
`nextWakeAt`; missed occurrences coalesce into one run, then advance beyond both
the delivered occurrence and current time. Clock rollback does not repeat an
occurrence; active wake timers recheck wall time at most every minute to detect
forward clock jumps. Empty supervisors still allocate no wake timers.
- [x] Desktop task details show block/failure reasons and attempts, offer Resume when
supported, and keep failed work visible. Existing supervisor change events refresh
the view; consumer results still enter conversations through the ordinary dispatcher
and continuation budget, with no new conversation store or wake protocol.

Acceptance evidence: `task-supervisor.test.ts` covers owner isolation, duplicate delivery,
clock changes, missed schedules, restart recovery, retry/deadline/resource policy, and
cancellation races. `long-task-wake.test.ts` covers normal dispatch and changed permission /
execution targets; `event-inbox.test.ts` covers durable consumer deduplication across crashes.
`supervised-tasks.test.ts` and `tests/e2e/supervised-task-recovery.e2e.ts` cover inspect/cancel/resume
and capture the real Electron task-details state.

### Follow-up — Campaigns + authenticated trigger adapters

- Add campaign records and bounded fan-out/fan-in over explicit repository/task sets.
- Persist immutable trigger envelopes; dedupe duplicate delivery across restart.
Expand Down
82 changes: 82 additions & 0 deletions docs/plans/unified-app-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Shared Apple and Android app running

Follow-on to #2667, on `codex/unified-app-run`.

## User flow

A detected local Apple or Android project offers **Run app…**. Opening it loads app metadata;
there is no pack toggle or project enrollment step for user-operated builds. One compact picker
contains App and Device, remembers the last valid project choice, and folds variants/configuration
into More options. Run builds, starts the device, installs, launches, and opens Desktop. Build and
Test remain explicit secondary actions. Progress names the actual stage and logs stream while it
runs. Stop app stops the launched app; closing Desktop leaves the device running.

Missing dependencies appear in the picker with an actionable recovery. Creation uses installed
runtimes; downloads are separate, explicit setup actions. No silent license acceptance, signing
changes, SDK installation, or emulator authentication downgrade. User-initiated Run opens a local
device ready for interaction. Agent-initiated presentation and remote desktops remain view-only.

## Implementation boundaries

- Share discovery result/selection/operation types, picker, status/logs, cancellation, persistence,
and presentation. Keep Apple and Android drivers independent.
- Reuse the installed Xcode driver; retain XcodeBuildMCP and its existing agent permissions.
- Android uses the project's Gradle wrapper, installed SDK tools, AVDs, authenticated loopback
framebuffer connection, and targeted adb commands. Discover Gradle application modules and
variants by evaluating their real configuration, not by guessing build filenames.
- Main resolves project/thread roots on every action. Renderer supplies identities, never arbitrary
host commands. Project build configuration and build scripts run with normal host access after
a user action, as existing Apple panel actions do. This does not grant agent permission.
- Save only project-relative app identities and device/variant preferences, never authentication
tokens. Operations are bounded and cancelled on shutdown; interrupted work is never replayed.
- Physical devices, remote toolchains, publishing, multi-touch, and Android agent tools are outside
this UI/lifecycle change. Existing platform tools stay available independently.

## Acceptance

- Apple and Android projects reach a shared Run picker without visiting Settings.
- Multiple apps, variants, compatible devices, and stopped devices are selectable; choices persist.
- Missing tools/runtimes and device creation/download are actionable in place.
- Both Run paths show real stages/logs and automatically present the selected local device.
- User-run control is immediate, agent/remote presentation remains view-only.
- Tests cover owner isolation, stale choices, failures, cancellation, setup consent, and presentation.
- Focused visual evidence covers picker, setup, progress, and running on both platforms.

## Validation and current limits

The shared flow was exercised through the real Electron titlebar and picker on macOS arm64 with
two disposable applications. Android used Gradle 8.10.2, AGP 8.7.3, SDK 34, and an Android 33
arm64 AVD; Run built, started the AVD, installed, launched, opened Desktop, and delivered a tap
to the guest app. Apple used an unsigned simulator-only SwiftUI sample and an iOS 26.5 iPhone 16;
Run built, booted the stopped simulator, installed, launched, and displayed its framebuffer.
Both opened with control enabled and made the Desktop toolbar visible from a disabled setting.
No personal app, physical device, signing profile, or personal AVD was used.

- [Android running in Desktop](../spikes/unified-app-run/android-running.png)
- [Apple running in Desktop](../spikes/unified-app-run/apple-running.png)
- Deterministic picker/setup/progress screenshots: `tests/e2e/screenshots/app-run-*.png`.
- Repeatable browser fixture: `tests/demo/app-run.demo.ts`.
- Desktop view-only behavior and separate device tabs: `tests/e2e/simulator-desktop.e2e.ts`.

This first shared UI supports local macOS hosts. Android Test runs the selected variant's local
unit tests; instrumentation tests and split-APK installation remain future work. Android runtime
downloads require SDK command-line tools and any licenses to be accepted in Android Studio.
The picker offers Debug/Release Xcode configurations. “App running” confirms successful launch;
it is not a continuous guest-process health monitor. Stop app stops the tracked app and leaves
the simulator/emulator available. Closing a dialog does not cancel a build; its progress and Cancel
action are available when reopening Run app. Closing Copse cancels unfinished operations.

The initial `pnpm run check` passed all 9,218 tests plus typecheck, lint, formatting, dead-code,
oracle, and syntax gates. The focused browser spec passed four cases; the Desktop spec passed
three, including two-device tab reuse. Fresh Apple and Android builds also passed through the
final picker/IPC implementation. The broader Electron run reached 72 passing specs before
`git-changes-image.e2e.ts` failed to display its proposed-image section. The same failure was
reproduced on the unmodified parent build (`fd3d969ca`), using that test's own Git fixture;
the remaining broad specs were not run. A separate follow-up demo fixture was corrected to use
an isolated workspace because a dirty checkout correctly adds an extra Changes bubble.

After rebasing onto the updated parent, `pnpm run check` passed all 9,282 tests and the build
passed. All 22 browser specs passed, including the four Run app cases. The three Desktop cases
and both image-preview cases passed; the parent now contains the image-preview fixture fix.
The full Electron suite has not been rerun. The API compatibility check against the parent passed
with protocol version 10, preserving its supervisor and Android protocol changes.
22 changes: 22 additions & 0 deletions docs/shell-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@ products remain in Copse-owned per-operation scratch directories, with ownership
duration, and log bounds enforced. A host restart invalidates the panel operation authority epoch,
so a recovered task cannot launch a second Xcode process whose predecessor may still be alive.

## Shared Run app workflow

The titlebar/project-menu **Run app…** flow is available for detected local Apple and Android
projects independently of agent-pack enrollment. Opening the picker authorizes loading the project
configuration (including Gradle configuration or Xcode metadata). Clicking Build, Test, or Run
authorizes the selected workflow and its project-controlled build scripts with normal host access.
The main process resolves the selected project/thread checkout and validates the discovered app,
variant, and device. It never receives arbitrary command lines from the renderer. This workflow does
not enable agent packs or create remembered agent-tool permissions.

The shared Apple picker defaults signing-profile updates off. Its explicit per-run checkbox adds
`-allowProvisioningUpdates` only to that operation; the existing Apple pack/MCP behavior described
above is preserved. Creating a device uses an installed runtime. Downloading an iOS runtime or an
Android system image is a separate labeled action. Android license agreements are not silently
accepted. External setup links open only the fixed Xcode/Android Studio destinations.

Operations have a duration bound and cancellable process trees. Logs are bounded; interrupted
operations are reported after restart and never replayed. A user-run local simulator/emulator opens
in Desktop with control enabled, scoped to the same project/thread. Agent-originated presentation
and remote desktops retain explicit view-only control. Closing Desktop leaves the device running;
Stop app targets only the app session launched by that workflow.

## Strict mode and expected blocks

`safetyExternalDenyThreshold` defaults to `1` (off). At a lower threshold, a command is hard-denied
Expand Down
Loading
Loading