Commit Graph

1246 Commits

Author SHA1 Message Date
Michał Pierzchała 6fd7576902 fix(ios): finish snapshot engine ownership move 2026-09-01 13:10:15 +02:00
Michał Pierzchała 44743351de refactor(ios): implement snapshot engine 2026-09-01 13:10:15 +02:00
Michał Pierzchała 61fe4a4d13 refactor(ios): consume shared snapshot presenter 2026-09-01 13:10:15 +02:00
Michał Pierzchała f97e9531e4 refactor(ios): extract snapshot presenter 2026-09-01 13:10:15 +02:00
Michał Pierzchała af2c65da1f refactor(url): split client and Metro URL ownership (#2210)
* refactor: split client and Metro URL ownership (#2146)

* fix: preserve URL ownership loading contract (#2146)

* fix: preserve Metro bundle URL construction

* fix: keep the moved base URL implementation verbatim
2026-09-01 12:25:42 +02:00
Michał Pierzchała fd4ab84166 refactor(ios): define snapshot acquisition and presentation contracts (#2203)
* refactor(ios): define snapshot acquisition and presentation contracts

* refactor(ios): isolate snapshot planning exports
2026-09-01 11:33:18 +02:00
Michał Pierzchała 81a9cb2b3c feat(daemon): establish interaction application facade (#2205)
* feat: establish interaction application facade (#2177)

* fix(daemon): narrow interaction runtime request seam
2026-09-01 10:46:54 +02:00
Michał Pierzchała 1522126f1f refactor: move close lifecycle behind session facade (#2212) 2026-09-01 10:25:59 +02:00
Michał Pierzchała b042045522 refactor(output): split presentation owners (#2202)
* refactor(output): split presentation owners

* fix(output): keep candidate rendering in surface owners
2026-09-01 07:50:25 +02:00
Michał Pierzchała 3afd154c80 perf: retain iOS runner across physical relaunch (#2200) 2026-08-31 21:41:30 +02:00
Michał Pierzchała 010f09bf0d refactor(daemon): move open lifecycle behind session facade (#2201) 2026-08-31 21:18:58 +02:00
Michał Pierzchała c86bf6feb5 refactor(cli): move process-exit lifecycle to CLI boundary (#2193) 2026-08-31 20:16:25 +02:00
Michał Pierzchała 28f80ddb40 refactor(cli): move update-check policy (#2194) 2026-08-31 20:16:16 +02:00
Michał Pierzchała 71e82aee77 test(kernel): move source-value coverage beside kernel owner (#2187) 2026-08-31 20:16:04 +02:00
Michał Pierzchała 5f5b8e827c refactor(app-log): move policy into daemon ownership (#2185) 2026-08-31 19:06:49 +02:00
Michał Pierzchała d53825a4b7 refactor(recording): move video validation policy (#2186) 2026-08-31 19:01:48 +02:00
Michał Pierzchała f3aabff12d refactor(snapshot): move Android helper presentation (#2184) 2026-08-31 19:01:39 +02:00
Michał Pierzchała 8591f47dd3 refactor: extract daemon session lifecycle inventory facade (#2183)
* refactor: extract session lifecycle inventory facade

* test: cover session inventory failure response
2026-08-31 19:01:00 +02:00
Michał Pierzchała afdbaa807f refactor(host-kit): move verified-file ownership (#2180)
* refactor(host-kit): move verified-file ownership

* fix(host-kit): update verified-file closure budgets

* chore(tests): remove retired exec fault fixture
2026-08-31 18:13:20 +02:00
Michał Pierzchała 1d5ddb54a3 refactor(snapshot): move snapshot policy utilities (#2135) (#2181)
* refactor(snapshot): move snapshot policy utilities (#2135)

* fix(snapshot): preserve CLI eager closure
2026-08-31 17:45:54 +02:00
Michał Pierzchała 7e784be98f refactor: move Swift cache policy to recording ownership (#2179) 2026-08-31 16:50:56 +02:00
Michał Pierzchała 8dba2b489d fix(daemon): compose stale-claim recovery from the dead owner's state dir (#2171)
* fix(daemon): compose stale-claim recovery from the dead owner's state dir (#2168)

The daemon's acquire-time reconciliation and startup sweep ran recovery
through the daemon's single composed gateway, so settling a foreign
state dir's dead claim cleared owned-process records by bare session id in
the RECONCILING daemon's own store — a live same-named session (default is
common across worktrees) lost its owned-processes.json to a foreign claim's
cleanup, while the dead owner's marker survived. Live-reproduced on main.

Every production reconciliation path — open-time admission, transient
admission, the startup sweep, and the daemonless CLI release — now goes
through createOwnerScopedDeviceClaimReconciler: one recovery composition per
claim, bound to that claim's recorded state dir (owned-process store and
session artifacts included), disposed after the transaction. Gateway state is
instance-scoped, so disposal shuts down only the transaction's own handles.

* test: prove the startup sweep composes recovery from the claim's state dir

Review follow-up on #2171: the unit tests exercised the owner-scoped
reconciler directly, so reverting the sweep callsite to the caller-scoped
reconciler stayed green. This regression boots the real daemon runtime with a
foreign dead claim whose recorded simctl recorder still runs as an orphan,
next to a live same-named session record in the daemon's own state dir
(purpose outside the startup reaper's selection). It proves the sweep
terminates the orphan, settles the foreign recording open -> completed,
clears the owner's process record in the OWNER's state dir, and leaves the
daemon's own record byte-identical. Observed red against a reverted sweep
callsite.

* test: prove open-time reconciliation composes recovery from the owner state dir

Review follow-ups on #2171:

- The acquire/open wiring had only live evidence. The new regression drives a
  real open through the production request router against a device whose
  foreign dead claim carries an orphaned recorder and durable recording
  evidence, next to a live same-named session record under the daemon's own
  state dir. It proves open-time reconciliation terminates the orphan,
  settles the foreign recording, clears the owner's process record in the
  OWNER's state dir, leaves the daemon's own record byte-identical, and hands
  the claim to the opening session. Observed red against a reverted router
  callsite.
- The startup-sweep regression now shuts its daemon runtime down in finally,
  so an assertion failure cannot leak the runtime.

* test: restore an absent claims-dir override by deletion, not assignment

Review follow-up on #2171: both new regressions (and the pre-existing
prune-log test they mirrored) restored AGENT_DEVICE_CLAIMS_DIR by plain
assignment, which turns an absent previous value into the literal string
"undefined" and leaks global test state. Absent values are now restored by
deleting the variable.
2026-08-31 16:48:20 +02:00
Michał Pierzchała f513b1d4ae refactor: extract daemon replay behind one application facade (#2166)
* refactor: extract daemon replay behind application facade

* fix: address replay facade review findings

* test: close replay ownership import scan gap

* fix: tighten replay capability boundaries
2026-08-31 15:43:33 +02:00
Michał Pierzchała 34e8cbb7a2 docs+ux: make device ownership discoverable end to end (#2165)
* docs+ux: make device ownership discoverable end to end

Complete the #1320 agent experience so 'busy? -> inspect -> choose or
release' is discoverable from every surface an agent actually reads:

- devices now projects the blocking claim owner per row (claimedBy with
  session and workspace, observe-policy projection; provably dead owners are
  excluded because the next open replaces them automatically), so an agent
  told a device is busy can pick a free one from the same listing.
- help debugging gains a 'Device busy and ownership' section separating the
  two DEVICE_IN_USE flavors and their exact recoveries.
- AGENTS.md documents both flavors; docs/agents/device-verification.md
  retires the last ps/kill recovery guidance in favor of device status,
  daemon stop --state-dir, and device release --stale (Stage 5 of #1320).
- ADR-0010 no longer calls DEVICE_IN_USE 'the only retriable code' without
  naming the claim path's non-retriable override.
- The rendered cross-worktree claim error gains a help-conformance quiz case
  binding (sample-output-device-claim-inspects-owner).
- README points at device status / device release --stale.

Part of #1320.

* fix: key ownership projection by canonical device identity end to end

Review findings on #2165:

- blockingClaimOwnersByDevice keyed claims and inventory rows by bare
  device.id, so a live Android claim could project claimedBy onto an
  unrelated same-id Apple/Harmony/Vega row, with scan order picking the
  displayed owner. Both sides now use the canonical local device key
  (claim.deviceKey against canonicalLocalDeviceKey of the row's claim
  identity). The cross-family same-id regression was observed red against
  the bare-id keying.
- The projection is now asserted across every hop the PR promises: client
  normalization preserves well-formed claimedBy and drops malformed ones,
  and the devices CLI formatter carries it through JSON data and renders
  the text line (MCP shares the same serialization).
2026-08-31 14:32:29 +02:00
Michał Pierzchała 2d2aa6c670 feat: add stale device claim release and dead-end recovery guidance (#2162)
* feat: add stale device claim release and dead-end recovery guidance

Close the #1320 recovery loop for claims no daemon can settle on its own:

- agent-device device release --stale settles a provably dead owner's durable
  resources through the same exact-owner reconciliation open and daemon
  startup use, then clears the claim last — daemonlessly, composing a
  local-only platform gateway in the CLI process. Live, uncertain, PID-reused,
  and corrupt claims always fail closed and are reported with the reason.
- DEVICE_IN_USE conflicts whose recorded owner provably cannot release
  (dead or superseded) now carry the exact release command as their recovery
  instead of a status inspection that dead-ended.
- device status --stale now offers the matching release command when provably
  dead owners are listed.
- daemon stop now warns in text output when a claim was orphaned (previously
  visible only via --json) and names the status/release commands.

Part of #1320.

* test: cover release refusal branches, text rendering, and orphan warnings

Changed-line coverage on the stale-release slice was 69.33% against the 70%
gate: the refusal-reason branches, the text-mode outcome rendering with the
live-owner hint, and the daemon stop orphaned-claim warning had no tests.
Cover them directly; the misnamed-claim-file refusal is also pinned.

* test: prove resources gate stale release and the scan-to-lock race stays closed

Review follow-ups on #2162:

- Two end-to-end CLI regressions run device release --stale through the real
  local gateway against a dead owner whose state dir holds an attributable
  durable screen-recording/app-log record: an owner-mismatched record and
  unreadable resource evidence both retain the claim (app-log-owner-mismatch,
  app-log-descriptor-invalid), proving cleanup must reach a terminal state
  before the claim can be deleted.
- A deterministic race regression holds the per-device claim lock while a
  release is in flight, replaces the claim with a successor before releasing
  the lock, and proves the transaction reports changed without reconciling or
  touching the successor's claim.

* fix: bind stale-claim recovery to the dead owner's state dir

Review P1 on #2162: the CLI composed one gateway from the caller's state
dir, so recovery for a foreign stale claim could clear the caller's live
owned-process record when both used the same session name — Apple recording
cleanup clears by session id through the gateway-composed store.

Recovery is now composed per claim, with the owned-process record store and
session artifact paths bound to the stale claim's recorded state dir, and
disposed after each transaction. The regression writes two dead claims with
one shared session name in different state dirs and proves each recovery is
composed from that claim's own state dir, never the caller's.
2026-08-31 14:32:29 +02:00
Michał Pierzchała 5fddf1ce14 feat(apple): reclaim retained runners under device-claim authority (#2160)
* feat(apple): reclaim retained runners under device-claim authority

Implement the #1320 retained-runner rule: a daemon holding the host-global
device claim may stop and replace a warm XCTest runner whose owner no longer
holds that claim. Previously worktree B's open failed with an unstructured
COMMAND_FAILED (IOS_RUNNER_OWNED_BY_OTHER_DAEMON) for up to five minutes
after worktree A closed, until A's daemon idled out.

- Runner leases now record deviceClaimProtocol: 1; takeover is gated on the
  lease declaring claim arbitration, so owners from pre-claims builds are
  never preempted.
- The claim-authority probe is daemon-bound through the existing runner-owner
  seam and answers from the claim store by process identity; unbound embedders
  answer false and keep today's refusal.
- Disposal now skips device-wide runner container-app termination when the
  on-disk lease is owned by someone else, so the losing daemon's idle stop or
  shutdown cannot kill the successor's runner on the shared simulator.
- Help topics updated: the live-owner runner rejection now names claim
  arbitration instead of being unconditional.

Live-validated with two daemons sharing one claim store against a throwaway
simulator: live-owner open still rejects with structured DEVICE_IN_USE;
after close, the contender opens in ~8s with the lease re-owned while the
loser daemon is still alive; stopping the loser afterwards leaves the
winner's runner healthy.

* fix(apple): make disposal ownership check and device-wide teardown one lease-locked operation

Review P1 on #2160: cleanupRunnerSessionResources read the on-disk lease
token and then terminated the device-global runner container bundles without
holding the runner-lease lock, so a loser could pass its check while a
successor's reclaim was in flight and then kill the successor's runner.

The ownership check, the container-app termination, and the lease release now
run as one operation under the runner-lease lock — the same lock a successor
holds for its entire reclaim-and-publish window — on every disposal path.
Callers already inside the lock (startup abort, lease-publish failure,
stopIosRunnerSession) declare leaseLockHeld instead of re-acquiring the
non-reentrant lock; if the lock cannot be acquired, all device-wide teardown
is skipped and an own unreleased lease turns stale on process exit.

The two-owner regression starts loser disposal inside a held successor
window, completes the takeover, and proves the loser neither terminates the
successor's runner nor touches its lease. Observed red against the pre-fix
code. Live-revalidated twice with overlapping daemon stop and takeover open
on a real simulator.

* fix(apple): match runner takeover authority by canonical device identity

Review P1 on #2160: the authority probe matched any active claim with the
same raw device id, while claims are canonically scoped by platform family +
Apple OS + id — an Android claim whose serial equals an Apple runner id could
authorize destructive takeover of a live Apple runner (and the prior probe
regression used an Android fixture, blessing exactly that).

The probe now receives the full DeviceInfo through the runner-owner seam and
the daemon answers with an exact canonical-local-device-key lookup: family,
Apple OS, and id must all match the held claim, which also replaces the store
scan with a direct keyed read. prepareRunnerLeaseForStartup takes the device
rather than a bare id so the lease seam can never degrade the match.

Adversarial regressions: a same-id Android claim grants no authority for the
Apple device (and the true Apple claim does) at the probe, and the lease-seam
test pins that the probe receives full family/OS identity, never a bare id.
2026-08-31 14:32:28 +02:00
Michał Pierzchała 3982cc865b fix: show named sessions in scoped inventory (#2164) 2026-08-31 14:30:30 +02:00
Michał Pierzchała 70fb783729 fix: reuse canonical Maestro visibility context (#2156)
* fix: reuse canonical Maestro visibility context

* perf: skip empty Maestro visibility filtering

* refactor: centralize snapshot visibility context
2026-08-31 13:41:15 +02:00
Michał Pierzchała f6d5757690 fix(maestro): measure settle outcomes directly (#2155)
* fix(maestro): measure settle outcomes directly

* test(maestro): prove settle metric execution
2026-08-31 13:37:04 +02:00
Michał Pierzchała b7e4b5aa7b fix: improve ref-based interaction recovery (#2167) 2026-08-31 13:36:48 +02:00
Michał Pierzchała 48eb1b06e3 feat: open Limrun uploaded apps (#2110)
* feat: open Limrun uploaded apps

* refactor: generalize deferred provider app selection

* refactor: fold limrun preinstall into allocation

* refactor: keep app selection provider independent

* refactor: enforce provider-independent commands

* refactor: restrict provider imports from commands

* refactor: keep provider guard lightweight

* fix: enforce provider-independent commands

* fix: enforce provider catalog lease admission

* refactor: deepen provider app catalog admission

* refactor: simplify provider capability projection

* fix: restrict Limrun uploaded apps on public daemon

* refactor: separate provider app catalog lookup

* fix: simplify uploaded app provider boundaries
2026-08-31 13:01:18 +02:00
Michał Pierzchała caa3dc23f9 refactor: dissolve caller-side src/replay into command and CLI owners (#2151)
* refactor: dissolve caller-side replay ownership

* fix: remove replay test-only export

* fix: restore replay loader promise boundary
2026-08-31 10:01:16 +02:00
Michał Pierzchała bf849ed0e6 fix: use canonical viewport fallback for rootless touch targeting (#2122)
* fix: use canonical viewport fallback for touch targeting

* test: complete viewport promotion fixtures
2026-08-31 10:00:56 +02:00
Michał Pierzchała f152827447 refactor(snapshot): move text-surface into snapshot presentation (#2152) 2026-08-31 08:10:48 +02:00
Michał Pierzchała dd988847c8 test: characterize replay application behavior (#2137) 2026-08-31 08:08:50 +02:00
Michał Pierzchała ed26b31c94 refactor: contract Apple platform surface (#2125)
* refactor: contract Apple platform surface

* refactor: use Apple plugin seam in tests

* test: ratchet snapshot handler size
2026-08-29 13:10:48 +02:00
Michał Pierzchała a6232e51cf refactor: prune platform split residue (#2123) 2026-08-29 13:10:47 +02:00
Michał Pierzchała ab20d5c2af refactor: retire platforms source seam (#2119) 2026-08-29 13:10:47 +02:00
Szymon Dziedzic d9677301f4 feat: add human takeover controls (#2078)
* feat: add human takeover controls

* fix: harden human takeover controls

* fix: align host XCTest selection count

* fix: address takeover readiness feedback

* fix: handle macos runner permission prompt in smoke tests

* fix: detect background macos permission dialog

* fix: dismiss inaccessible macos privacy sheet visually

* fix: map macos privacy prompt without accessibility

* refactor: own human-control holds in lease registry

* fix: cancel pending human takeover on disconnect
2026-08-29 13:09:54 +02:00
Michał Pierzchała 69001179b0 fix(daemon): confine remote HTTP trust boundaries (#2111)
* fix: confine remote HTTP trust boundaries

* refactor: simplify trust policy plumbing

* fix: remove remote host path install opt-in

* test: cover remote HTTP trust boundaries

* test: attest remote RPC tenant fixtures

* test: cover malformed network addresses

* fix: constrain proxy HTTP network policy

* refactor: preserve fetch semantics in remote HTTP
2026-08-28 21:37:03 +02:00
Michał Pierzchała 9abcd7fe03 refactor: move Apple platform family into package (#2118)
* refactor: move Apple platform family into package

* fix: preserve Apple facade sync contracts

* fix: complete Apple W4 rebase review fixes
2026-08-28 15:25:44 +02:00
Michał Pierzchała c7f42ccedc refactor: move Android family behind package exports (#2117)
* refactor: move Android family behind package exports

* fix: address Android W5 review feedback

* fix: update relocated routing fixture assertion
2026-08-28 13:02:28 +02:00
Michał Pierzchała 838ed223b5 refactor: move W6 platform families behind package facades (#2116)
* refactor: move W6 platform families behind package facades

* fix: address W6 loading and composition review
2026-08-28 12:46:39 +02:00
Michał Pierzchała 7b8578ec8e fix(daemon): enforce provider session artifact ownership (#2109)
* fix(daemon): enforce provider session artifact ownership

* fix(daemon): anchor provider session retention at expiry

* fix(daemon): preserve expiry for late provider sessions

* fix(daemon): close provider retention clock boundary

* fix(daemon): type sequenced expiry clock
2026-08-28 11:50:03 +02:00
Michał Pierzchała 315a0ac679 fix(daemon): fail closed when the auth hook is silent about tenant (#2104)
An auth hook that ran but returned no tenantId opted the deployment into
tenant attestation; falling back to the client's own claim (RPC body
meta.tenantId, aux-route x-agent-device-tenant header) let a holder of one
valid shared token impersonate any tenant on /rpc and on the diagnostics/
upload/download routes. resolveTrustedTenant() in the new
src/daemon/server/tenant-trust.ts is now the single seam both surfaces go
through and the only place that computes the resulting identity: hook
attests -> use it; no hook configured -> keep today's client-declared
behavior (loopback/dev unchanged); hook configured but silent with a
client-declared tenant -> refuse (401) instead of trusting the claim, and
no raw client-declared metadata survives into the dispatched request in
that case either.

Fixes #2095
2026-08-28 11:50:02 +02:00
Michał Pierzchała af6f12e391 chore: adopt shared oxlint config (#2115)
* chore: adopt shared oxlint config

* fix: preserve project lint boundaries

* fix: remove redundant oxlint config
2026-08-28 11:42:58 +02:00
Michał Pierzchała 5e30451b72 fix(daemon): let a retried open supersede the claim its aborted attempt abandoned (#2105)
* fix(daemon): let a retried open supersede the claim its aborted attempt abandoned

An `open` that fails after preparation keeps its device claim: the effects it
may have started are unproven, so releasing the device would hand an unknown
state to the next session. Nothing recorded that the claim binds no session,
so it stayed on disk for the daemon's whole life and every later open on that
device failed with DEVICE_IN_USE naming a session that no longer exists. A
`test --retries` run spent its whole retry budget on that conflict.

Rolling an unowned claim back now marks it abandoned instead of leaving it
untouched. Other processes still read a live claim, so the host-global fence
is unchanged; the daemon that abandoned it supersedes it on its next acquire.

* fix(daemon): stop an abandoned claim covering a transient-exclusive command

`acquireTransientDeviceClaim` treated any claim of this daemon as coverage, so
an abandoned open claim let install/push/prepare/shutdown run without taking
ownership and left the abandoned record behind. Only a claim that still holds
the device covers a transient command; an abandoned one is superseded under the
claim lock into the command's own transient claim, which its dispose releases.
2026-08-28 08:33:19 +02:00
Michał Pierzchała ddb415a2c7 refactor: sink package-closed src modules into existing packages (#2106)
* refactor: sink package-closed src modules into existing packages

Move closed modules into contracts, kernel, capture-kit, and ad-script,
and declare DaemonCommandDescriptor in core so R6/R9 can pin the remaining
provider-webdriver type cycle.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: keep contracts and capture-kit off generic sinks

Move interaction-outcome, snapshot warning rendering, and inventory ALS
behind focused owners, and plant R18/R70 domain-shape gates so they
cannot return as package export-map growth.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: drop moved implementation comments from owner modules

Names, types, and tests already carry those invariants; the relocated
files should not keep review-history or control-flow narration.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: drop the empty snapshot-quality layering zone

W1 moved the verdict into capture-kit and this PR moved warning rendering
into snapshot-presentation, so the ranked zone no longer has production files.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 07:56:18 +02:00
Huangshuo Kuang 2e87347bee fix(wait): surface runner restart timeout evidence (#2113) 2026-08-28 07:48:10 +02:00
Michał Pierzchała 782f4e2e34 fix(daemon): dispose late durable capture authorities (#2107)
* fix: dispose late durable capture authorities

* fix: preserve late authority control cleanup

* fix: preserve aborted recovery cleanup
2026-08-28 07:47:23 +02:00