# Remozio implementation plan

Status: draft, revision 45. Revision 45 addresses eight Kimi review findings. Final verification is pending; the GLM revision 44 attempt returned no report. Revision 44 consolidates the agreed self-hosted packaging, setup export, independent enrollment, ADB authorization, distribution, and settings decisions. The user completed the human design review and selected the platform baseline and native stack below. K3, Opus, and GLM reviewed the frozen revision 29 approval plan. Kimi and GLM separately reviewed the ADB changes through revision 37. Additions in revisions 38–45 await independent review. Platform experiments and the remaining product decisions are still open. Review evidence: reviews/REVIEW-LOG.md. Date: 2026-09-30.
Target: one Android app instance managing approvals across multiple Macs, with multiple Android devices supported.
This is a standalone project. IntelliJ Community rules and build tools do not apply.

## 1. Product contract

### Approval flow

When the routing policy selects the phone, the user receives a notification that approval is needed. Opening it shows the actual request.

One explicit decision authorizes one captured action. Sensitive actions require one strong biometric; the exceptions below do not.

The phone reports success, failure, or an uncertain outcome. The Mac remains usable through existing manual approval methods.

### Supported requests

Support three request types: privileged command execution, 1Password unlock/access dialogs, and Little Snitch connection alerts. Full command details are required for command requests.

UI requests show all available relevant details and explicitly identify unavailable fields. Section 19 proposes a separate, opt-in ADB deployment bridge; it is not an approval request type.

### UX changes need consultation

Do not silently sacrifice usability to satisfy review findings. Changes that add confirmations, remove remote functionality, restrict supported commands, shorten practical response times, force repeated enrollment, or add manual steps require user consultation. ADB is disabled by default for each Mac–Android pair. Explicitly enabling a pair permits unattended use until disabled or revoked, without per-session or per-install confirmation.

Record such proposals as pending decisions. Do not claim review convergence while substantive planning decisions remain unresolved.

A defined feasibility gate with its required evidence and failure/consultation path is planned implementation work, not a missing planning decision.

### Normal interaction

Normal interaction: notification -> request details -> explicit decision -> biometrics when required -> result.

| Action | Biometrics |
| --- | --- |
| Dismiss or decline the Remozio request, without a target-app action | No |
| Cancel the current target request, without granting access or creating a reusable rule | No |
| Little Snitch deny-once, affecting only the current request | No |
| Little Snitch allow-once, affecting only the current request | No |
| Little Snitch Allow or Deny that creates, changes, or removes a reusable rule | Yes, for every decision |
| Little Snitch default Forever decision | Yes |
| Privileged command approval, 1Password unlock/access, or credential release | Yes |

### Scope and inbox behavior

The user confirmed cancel, deny-once, and allow-once without biometrics, and all rule-setting decisions with biometrics. That exception is specific to Little Snitch; it does not weaken command or 1Password approval.

A session rule or timed rule is reusable and requires biometrics, even though it is temporary. Preserve the existing Forever default.

Do not silently select Once to avoid authentication. Provide an inbox for concurrent requests, per-Mac labels, clear offline state, cancellation, and readable errors.

No mandatory warning modal or forced scrolling for routine requests. Long commands remain fully accessible and are never silently truncated.

Signing remains available only for the request currently displayed.

### One Android app for all Macs

One Android app instance manages all enrolled Macs and accounts. Each Mac can also enroll multiple Android devices. No separate app installation or profile is required for each Mac.

The default inbox combines requests from all enrolled Macs. A Mac/account filter narrows the list without reconnecting, switching credentials, or hiding notifications from other Macs.

Every request card and detail view shows the Mac and account. A notification opens its exact request regardless of the current filter. New arrivals never replace an open request or its biometric operation.

The Devices destination lists each Mac and account, connection state, last contact, routing mode, effective destination, and presence reason. Mark stale observations as last known, not current. Connection failures on one Mac do not block requests from another.

Each online Mac/account offers Set Away. The Android app cannot select Automatic or Present. Those controls exist only on the Mac, including through remote desktop.

| Example enrollment | MacBook | Mac Studio |
| --- | --- | --- |
| Android phone | Paired | Paired |
| Android tablet | Paired | Not paired |

*The phone receives eligible requests from both Macs. The tablet receives only MacBook requests. Each Mac resolves its own requests independently.*

Names are editable labels, not identities. Show a stable distinguishing suffix when labels and account names collide. Renaming a Mac preserves pairing and history.

Mac activity and routing overrides apply separately to each Mac/account. Using one Mac must not suppress requests from another idle Mac.

## 2. Scope and threat model

### Threat boundary

Assume an uncompromised Android OS, Mac kernel, privileged service, and installed signed app. Protect against network interception, message tampering, replay, stale dialogs, accidental focus changes, and a malicious unprivileged requesting process.

Root compromise and a compromised phone OS are out of scope. UI automation cannot provide the same assurance as direct execution; document that limitation clearly.

A caller whose host already has Accessibility or Automation permission over the target may bypass Remozio and act directly. Enabling Little Snitch GUI scripting broadens that exposure.

Remozio authenticates its own actions; it cannot enforce global phone approval over other authorized automation. Explain the relevant grants during setup and inspect only what public APIs permit.

The optional ADB bridge is a separate developer capability. ADB host keys are commonly accessible to processes running as the same Mac user. A local process with that key and access to an active bridge could control the phone through ADB without a Remozio approval. The bridge cannot promise protection against a malicious same-user process unless its session-admission policy provides an independent phone-side authorization. The user selected unattended ADB for explicitly enabled pairs. Explain this residual same-user exposure during setup; do not add recurring approval prompts.

Do not revoke existing terminal/IDE grants without user consultation.

### Who can assert a request

The local relay is unprivileged and cannot mint approvals or authoritative requests. Mac authorization keys and enrolled phone public keys live in root-controlled storage.

The execution service independently validates signatures and owns execution records. UI details are assertions from an authenticated, signed adapter, not claims supplied by the requesting coding agent.

### What a signature proves

A valid action signature proves authorization by an enrolled phone key. Only the biometric key supplies per-action biometric assurance.

A decision without biometrics proves an explicit app decision under the trusted-phone assumption, not the current operator's identity. It does not prove a script, executable, dependency, or remote resource is harmless.

Command approval binds an invocation, not the full transitive effects of arbitrary code.

### Deployment scope

Initial deployment: personal Mac and Android. Use direct communication on the same LAN when reachable; use an internet relay otherwise.

No VPN or WARP client is required. Do not require physical proximity after enrollment. The optional ADB deployment bridge in section 19 has its own Android debugging prerequisites and availability state.

FileVault preboot, OS login, and universal interception of protected macOS dialogs are outside the initial implementation. They need separate platform support.

### Platform baseline and support

Support macOS 26 and later on Apple Silicon only. Ship arm64 Mac binaries; Intel Macs and older macOS versions are outside the initial support scope.

Target Android 17 and later with native Kotlin and Jetpack Compose. Google Pixel devices are the supported and tested Android family. Other manufacturers are best effort and may have feature limitations.

Publish the tested Pixel model and OS matrix before claiming release support. Validate the required capabilities on those devices; the Pixel name alone is not evidence of compatibility.

Permit installation on other Android devices with a clear support note. Detect missing capabilities and disable only affected features where safe. Do not describe untested devices as supported.

### Self-contained deployment and distribution

This is a self-hosted homelab application with one deployment per user, spanning independent Macs and Android devices. There is no SaaS or mandatory central registry, and no Mac has a permanent main role.

Distribute a single signed, notarized Mac .app containing the UI, privileged helper, login/background services, push sender, relay endpoint, and required tunnel client. Setup registers the appropriate launchd services. Process isolation remains mandatory; one app bundle does not mean one process.

No Docker, Raspberry Pi, or separately installed application server is required. Each Mac independently sends pushes and exposes its own authenticated endpoint. A shared Firebase project is permitted, but an existing Mac must not remain necessary after another Mac is configured.

Use GitHub Releases for initial downloads. Mac updates use Sparkle and the coordinated service activation rules. Android initially uses signed sideloaded APKs, with in-app update detection, download, and handoff to the Android installer. Preserve the Android signing identity to retain data and pairings. Verify the package identity, version, and signing identity before offering installation; reject unexpected signing changes. Silent installation is optional where supported, not promised. Google Play may follow later.

### Portable setup export

Any configured Mac can export a versioned setup bundle for another Mac’s onboarding. Include shared FCM/provider configuration, shared settings, and credentials needed to provision new machine-specific resources. Show the included categories before export.

Encrypt and authenticate the bundle using an established password-based format with a salt, appropriate password derivation, and authenticated encryption. Select and review the concrete format during implementation. Never write plaintext staging files or log its password or secret values. Import through protected setup, validate the format and limits, and preview the settings and provisioning scope.

Do not export authority private keys, ADB host keys, device-specific operational keys, pairings, ADB pair grants or enabled state, approval credentials, audit history, or pending requests. Create a new Mac identity, independent keys, endpoint, tunnel registration, and phone enrollments on import. Do not reuse another Mac’s tunnel run credential as the new machine’s identity.

The source Mac has no ongoing role. Explain that copies of an export retain its shared secrets and need protection; removing the file is not credential revocation. Prefer separately provisioned credentials where providers support them. Rotating shared provider credentials must expose which Macs need updated configuration.

### Independent enrollment and re-enrollment

Each Mac maintains its authorized Android devices; each Android device maintains its paired Macs. No central registry is required. Re-enroll after reinstall, replacement, or lost identity keys, and explicitly retire stale identities. Matching names never establish continuity.

Either endpoint can immediately stop trusting its peer locally, then notify the peer when reachable. Revocation on one Mac does not imply revocation on all Macs. A coordinated removal action, if provided, requires explicit management authority for each target and shows confirmed or pending/offline separately. Approval authority alone does not grant fleet management permission.

### Configurable behavior

Expose behavior settings in the apps, with shared defaults and per-device overrides where appropriate. Include shared defaults in setup exports. Cover idle timing, notifications, update checks, connection behavior, and supported retention choices. ADB pair enablement is separate per-pair authorization state, never an exportable or inheritable default. Every new pair starts disabled and requires an explicit enable action after enrollment. Show the effective value and its source.

Settings cannot weaken signature validation, replay prevention, or the agreed biometric action policy. Android routing actions remain limited to Set Away; Automatic and Present are Mac-only. Any Android-editable tuning must also preserve that boundary and must not become an indirect switch that suppresses phone delivery. Keep delivery-suppressing presence tuning on the Mac.

An offline Mac shows Offline and last contact. Optional cached details are labelled Last known; neither status nor controls may imply current knowledge or successful remote changes.

## 3. Components

- Android Kotlin/Compose app: pairing, approval inbox, detail renderers, biometric signing, credential release, connection status.
- Signed Swift macOS app and per-user GUI agent: UI observation, notifications/status, and Accessibility adapters.
- Dedicated unprivileged background transport: phone connectivity for command requests without an Aqua login. UI adapters remain scoped to the correct logged-in GUI session.
- Small privileged execution service: trusted request capture, authorization policy, signature verification, durable action state, and process execution. No UI automation or general network listener in this service.
- Command frontend: explicit `remozio sudo -- <executable> <args...>` integration for coding tools. No initial replacement of `/usr/bin/sudo`, blanket NOPASSWD rule, or PAM-wide authentication bypass.
- Separate signed administrative setup controller: scoped cloud provisioning through administrator-authorized local operations; no network listener or approval authority.
- Shared protocol specification and cross-platform test vectors. Use maintained standard cryptographic implementations.
- Optional development bridge: a phone-side connection to its own wireless ADB TLS port and a separate Mac-side transport endpoint and command. It has no approval authority; see section 19.

### Native Mac implementation stack

Use Swift for the Mac application, user-session adapters, network transport, privileged authority, command frontend, and separate ADB bridge. Use SwiftUI for the interface and AppKit where native window or menu behavior needs it.

Keep the process boundaries defined in this plan. Shared language and modules do not merge the privileged authority with UI automation or network-facing services. Services must not depend on SwiftUI or an open application window.

Call Accessibility and other C-based platform APIs from Swift. Add small C or Objective-C wrappers only when a concrete integration needs them. No substantial Objective-C layer or shared Rust core is planned initially.

Use Xcode targets for signed application and service bundles, and Swift packages for reusable modules. Share protocol definitions, test vectors, and conformance cases with Android. Do not add Kotlin Multiplatform solely to share Mac authority code.

Start service-registration experiments with SMAppService and local communication with authenticated XPC. Use CryptoKit and Security.framework for supported cryptographic and identity operations. Their suitability for pre-login execution and protected key custody remains an evidence gate.

Use SQLite with explicit transactions and migrations for durable authority and audit state. Swift concurrency coordinates in-memory work; it does not replace durable atomic consumption or the independent rollback checks.

Distribute Developer ID-signed, notarized Mac releases with Sparkle 2 updates and the existing coordinated activation rules. Select concrete networking, database wrapper, and protocol libraries after the relevant experiments. These library choices must preserve the agreed behavior.

### Authenticate local process boundaries

![Process architecture and privilege boundaries](site/dist/figures/components.svg)

*Only the root service owns approval authority. The transport moves encrypted messages; the GUI adapter needs a one-use permit.*

Use authenticated XPC with audit-token and code-signing checks for the app-to-service boundary. Release app/helper bundles use hardened runtime and library validation, without debugging, task-access, unsigned-code, or DYLD-injection exceptions.

Install trusted bundles and their update paths in protected locations. “Administrator-controlled” must not mean writable by unprivileged processes running as the interactive administrator account.

Root-launched code, containing directories, service registrations, and activation staging must resist replacement by those processes. Use root-owned, non-user-writable placement, or prove equivalent platform enforcement of both approved code identity and anti-rollback at activation and launch.

Signature validity alone is not equivalent protection: an older signed authority must not replace the current root executable through a same-user writable path.

Validate live code identity and validity for each privileged XPC request, not only at launch.

Test same-user injection and bundle modification against these boundaries. The command frontend may be invoked by an untrusted same-user process: do not mistake its signature for user consent.

Capture the actual caller UID and process identity at the service boundary. Root-controlled policy permits only explicitly enrolled administrator accounts and target identities; installation must not turn every local user into an administrator.

Preserve existing managed-machine policy: deployment checks must surface incompatible authorization policy rather than bypass it.

### Root authority and retained state

The root service owns the Mac authority key, phone trust anchors, immutable request records, and consumption ledger for all request types. It signs only validated protocol objects generated from its own retained records, never arbitrary caller bytes.

For UI requests it authenticates the GUI adapter's audit token, user/session, and designated code requirement before endorsing its captured assertion. A root signature attests adapter provenance, not atomicity of Accessibility observations.

### Key custody and pre-login transport

The phone verifies that authority signature before showing an actionable request. Give transport authentication its own key, separate from approval authority.

The Mac authority key and every key that certifies credential recipients must be device-bound and non-exportable. Prove that backup restoration or migration cannot clone their signing authority onto another Mac.

Evaluate Secure Enclave protection with the required pre-login availability and code-identity restrictions. A this-device-only storage label is not proof of non-exportability; validate its actual protection. No ordinary-file or exportable-key fallback is permitted for these roles.

Add backup-clone impersonation and recipient-certification tests to the key-custody feasibility deliverable. If the platform cannot meet custody and availability requirements together, retain a blocker and consult the user before changing the security or UX contract.

Migration to different hardware needs a new authority key and authenticated continuity or independent recovery. Preserve pairing records where safe; do not trust an exported old private key or a self-asserted replacement.

Use explicit re-enrollment for hardware replacement or lost authority keys. Retire the old identity and enroll the replacement through the normal authorized setup flow. Preserve trustworthy records where possible without copying authority keys. Routine updates preserve the existing identity without new enrollment steps.

Adapter receiving keys must be non-exportable and code-signing/access-group restricted; use Secure Enclave protection where compatible. Prefer the same protection for transport keys.

The pre-login transport runs as a LaunchDaemon under a dedicated service account, not as the interactive user or an Aqua LaunchAgent. If pre-login keychain use is unavailable, a transport-only key in dedicated-account-private storage is an acceptable fallback: no interactive account can read it, and all containing directories and service registrations are administrator-controlled.

This fallback does not apply to credential-receiving or authority keys. Validate pre-login availability and same-user isolation, and record the chosen key custody in the feasibility deliverable.

If neither transport path meets these requirements, retain an explicit blocker and consult the user; do not silently require GUI login or expose keys. The root authority certifies the receiving adapter's public key through authenticated XPC, binding it to the account, session, process incarnation, and request.

Credential packages are encrypted to that recipient and authenticated as part of the approved transaction, not released as plaintext to the transport relay. A same-user replacement binary or changed LaunchAgent registration cannot use the identity or receive a credential package.

### Independent adapter verification

The GUI adapter independently verifies the approval and obtains an atomic one-time dispatch permit from the root ledger immediately before acting. If it crashes after dispatch, the result is unknown and the action is not retried.

For unlock requests it also verifies credential-package bindings. No general signing, decryption, or unrestricted dispatch RPC is exposed.

The trusted adapter can access the credential it must enter; compromise of that signed process is outside the stated assumption. Relay compromise can delay/drop traffic and may expose transport metadata, but cannot modify an authority-signed request or decrypt a credential package.

Minimize request-detail exposure to the relay with end-to-end payload encryption between the root authority and phone.

### Availability by session state

Command transport starts as a dedicated service without GUI login, including for an independently authenticated SSH admin session after OS boot. A locked GUI session must not stop command requests.

UI requests need the matching GUI session and return a clear unavailable status when it is absent. FileVault preboot remains outside scope.

Tests must show that command approval does not depend on GUI adapter availability.

### Feasibility gate: service installation

Privileged service installation, update, removal, permissions, and code-signing requirements are an early feasibility spike. Select supported Apple service-management APIs after validating the supported macOS versions.

Test macOS 26+ on Apple Silicon, matching the supported platform baseline. Record tested OS builds; do not infer support from older-system results.

## 4. Borrowing from Flux

### Inspected source

Reference: https://github.com/bjarneo/flux at 78df3c46b2a5f567ce71b3d9bcd5f6d5854520e5. Relevant files: Android ApproveKeys.kt, ApproveActivity.kt, Approvals.kt; Go internal/approve/{helper,message,keyfile,client,pam}.go; docs/approve.md.

### Architecture reuse and license

Borrow the architecture: biometric-bound signing, separate enrollment and approval messages, fresh challenges, root-owned verification keys, independent privileged verification, and bounded timeouts. Reuse reviewed code only after confirming a license that permits reuse.

The inspected repository guidance says no license was selected. Until resolved, use the design as a reference and independently implement from platform documentation; do not copy source into Remozio.

### Linux-specific behavior to exclude

Do not import Flux's broad desktop feature set. Do not reuse its command-blind PAM approval or Linux-specific installation/socket assumptions.

Its macOS app is a client for Linux, not the host needed here.

### Hardware assurance limits

Flux lacks remote key attestation. Remozio initially trusts the enrolled Android app and records the locally verified Keystore security level.

StrongBox is preferred; hardware-backed TEE is acceptable. Software-only hardware assurance, biometric fallback, and recovery changes require a user decision before implementation.

Do not promise remote hardware attestation in v1.

## 5. Pairing, transport, and phone lifecycle

### Enroll, add, or replace a phone

![Pairing sequence](site/dist/figures/pairing.svg)

*Enrollment requires administrator authorization and the phone biometric. Registering for push does not enroll an approval key.*

Enrollment requires authorization verified locally on the Mac and a phone biometric. An independently authenticated remote administrator session, including SSH or screen sharing, can perform this setup; physical presence is not required.

Exchange pinned identities with a QR code and a human-verifiable code. Provide an equivalent text/code pairing route for a phone that cannot scan its own remote-session screen.

Privileged enrollment binds the phone verification key to the Mac account after local administrator authorization. Network messages alone cannot enroll, replace, or revoke trust anchors.

Assign stable random Mac identities; hostnames are display labels. Permit multiple enrolled phones for one Mac/account tuple.

When phone routing applies, deliver each request to every currently authorized phone over its own authenticated, encrypted channel. The first valid decision consumes the request; all other phones reconcile the terminal result and cannot act again.

Enrollment clearly distinguishes adding a phone from replacing a named phone. Replacement revokes the selected old phone in the same administrator-authorized operation; routing never silently removes or grants authority.

Show the enrolled device list and explicit revocation state.

### Keep each Mac enrollment independent

Adding a Mac creates another enrollment in the same Android app. It does not replace existing Macs or grant access to them. Adding a phone enrolls it separately with each intended Mac/account.

Keep pinned authorities, authorization keys, credentials, transport configuration, and recovery state scoped to the Mac/account enrollment. Apply this isolation to caches, notification IDs, deep links, and queued decisions.

Use the authenticated Mac/account identity with the request ID as the app's request key. Never merge requests by hostname, display name, or request ID alone.

A shared Android push token can serve several enrollments. Rotation updates each authorized registration independently. Revoking one enrollment removes only that mapping, not other Macs' delivery registrations.

Revocation on one Mac does not revoke the phone on other Macs. Clearly show this scope in device management. Losing a phone requires revocation on every affected Mac.

Negotiate protocol support and report update needs separately for each Mac. An incompatible or offline Mac cannot disable compatible peers in the same app.

The optional ADB bridge retains its separate grants and session rules. Pairing another Mac for approvals does not grant ADB access to an Android device.

### Background delivery

Use end-to-end authenticated encryption with pinned device identities. Push notifications are a required baseline for background delivery to registered phones.

A persistent phone connection or foreground service must not be the only delivery mechanism. The Mac background transport is independent of its GUI adapter.

Validate Doze, background restrictions, force-stop behavior, notification permissions, and LAN-to-mobile transitions on a real device. No claims of guaranteed delivery when Android or the push provider prevents delivery.

### Push and reconnect

When the Mac routing policy selects phone delivery, the notification service sends a push to each registered, currently authorized phone. Push payloads contain only opaque wake identifiers; fetch request details over the authenticated channel.

The relay never receives plaintext request details or credentials. Queue requests on the Mac while disconnected; show the caller that approval is waiting for connectivity.

Reconnect reconciles request states without replaying actions. Push registration is delivery metadata, not enrollment or approval authority.

### Request lifetime and queue limits

Expire requests after a provisional five minutes, adjustable within defined resource bounds. Show remaining time and permit explicit resubmission.

This duration is a UX default to validate, not a reason to force rapid responses. Bound outstanding requests and notification rate per caller and Mac; retain visible queued requests rather than flooding biometric prompts.

Never auto-approve a batch.

### Revocation and pairing recovery

Pairing recovery: an independently authenticated administrator session can revoke a lost phone and enroll a replacement, locally or remotely. The existing phone approval must not be required to repair its own enrollment.

Existing manual administrator authentication remains the recovery path; the phone transport cannot authorize enrollment by itself. Phone-side removal prevents future signatures but must also explain Mac-side revocation; it does not cancel requests on the Mac by itself.

Mac-side key removal immediately invalidates only that phone's authority over pending requests. Keep those requests available to other authorized phones and synchronize their state.

Cancel a pending request only when no authorized approval surface remains for that Mac/account. An available, implemented local approval path keeps the request pending after the last phone is revoked.

A planned but unimplemented local path does not count. Native target dialogs retain their existing local behavior even when Remozio cancels its request.

Serialize revocation with decision consumption so a removed key cannot win a later transition.

The user prefers keeping pairing after biometric changes where practical. Preserve device identity and pairing metadata; do not mandate full re-pairing merely because fingerprints change.

The feasibility work must select a supported biometric-key policy and authenticated recovery that retain pairing when they meet the threat model. Do not assume all Android versions or key configurations preserve keys across enrollment changes.

If the OS invalidates a key, show exactly which actions are unavailable; do not export or recover the invalid key. Existing non-invalidated decision-key authority may continue for one-time actions unless the enrollment is revoked or its identity is untrusted.

A decision key alone must never authorize replacement of a biometric or credential key. When safe continuity is unavailable, use the already-supported independently authenticated remote-administrator recovery to replace affected trust within the retained pairing record.

Require full pairing only if that record itself cannot be trusted or restored. If credential keys are invalidated, retain the device pairing but require explicit credential re-entry.

Deliver supported-version behavior and the concrete recovery steps before implementing an exceptional UX change.

## 6. Signed request and state machine

### Canonical encoding and size limits

Define a versioned, deterministic wire representation with cross-language vectors, explicit byte limits, and strict parsing. Reject duplicate fields, unknown critical fields, unsupported versions, and ambiguous encodings.

Never truncate payloads at capture, transport, parsing, or display. Set documented limits from measured supported-platform argv/environment bounds plus protocol overhead and realistic UI captures.

A legitimate capture above a limit returns a distinct Request too large result with the exceeded limit to the caller and phone when reachable; it never becomes a generic denial, malformed request, or silent wait. Prefer deterministic CBOR using a precisely documented subset.

Never sign a rendered shell string or arbitrary JSON serialization. Section 18 defines version negotiation, compatibility declarations, and evolution rules.

### Request and decision bindings

| Signed object | Signer | Bound values |
| --- | --- | --- |
| Request | Mac authority | Mac/account, challenge, request ID, canonical payload, digest, expiry, permitted choices |
| Decision | Enrolled phone key | Full request digest, challenge, chosen action/scope, phone identity, key ID and purpose |
| Status | Authenticated status channel | Request identity and authoritative state; never approval authority |

Define two structures. The authority-signed request contains protocol/purpose version, request type, request ID, Mac/account binding, a 256-bit random challenge, creation/expiry times, immutable payload and digest, and captured permitted action/scope choices.

It does not preselect the responding phone, signing key, or action. The phone-signed decision binds that complete request digest and challenge to the selected action/scope, phone identity, enrolled signing-key ID, and key purpose.

Both verifiers derive the required key class from retained action semantics and check the key against the enrolled purpose; a purpose asserted in the message alone grants no authority. Separate biometric authorization, one-time UI decisions, request cancellation, enrollment, credential release, routing control, update scheduling, gateway control, and status messages by purpose.

The action payload includes every choice that changes behavior, including firewall rule scope.

### Digest verification and time

The authority creates the immutable request. Android recomputes its digest from the canonical payload it parses and renders; it rejects a supplied digest mismatch.

It signs that digest and the selected action. Each acting authority recomputes the digest against its retained original.

A trusted monotonic clock that includes time spent asleep controls expiry on the Mac; wall time is informative and supports protocol validation. Clock skew must not extend authority.

Signatures are single-use, with no reusable sudo authentication window.

### Both verifiers enforce scope

The root service and GUI adapter independently enforce the action/scope table in section 1. Classification comes from the retained action semantics and trusted adapter capability mapping, never a caller-provided label or phone assertion that biometrics are unnecessary.

Any reusable rule change requires the biometric key, including Deny Forever and temporary/session rules. Command execution, 1Password access/unlock, credential release, and enrollment also require that key.

### The narrow decision key

Use a separate non-exportable Android decision key for biometric-free decisions. Enroll and bind it to the same Mac/account tuple, with only the narrow permitted purposes.

Its signatures bind the full immutable request, challenge, action, scope, expiry, and phone identity. Verify them end to end at the root service and independently at the GUI adapter.

The relay and transport keys cannot mint these decisions. This key cannot authorize rule changes, command execution, 1Password approval, credential release, or enrollment.

The allow-once exception authorizes only that captured Little Snitch request. Sections 14 and 17 separately define narrow routing and update-scheduling controls under this key. They grant no target-action, enrollment, or release-signing authority.

### Cancellation authority

Cancellation of Remozio itself conveys no target-app authority. A target-app cancel, deny-once, or allow-once does authorize a narrowly scoped external UI action; it must use the dispatch ledger.

Status messages remain authenticated separately and cannot authorize actions. The root service validates account, request, and enrolled key identity before accepting each decision.

Only the originating authenticated caller/session, an enrolled authorized phone, or the local administrator may cancel a pending request. Caller cancellation uses verified IPC identity rather than a caller-supplied PID.

Availability attacks cannot turn denial or cancellation into approval.

### Atomic state transitions

![Request state transitions](site/dist/figures/states.svg)

*The consumption boundary separates safe pre-dispatch expiry from outcomes that require evidence. Unknown is terminal for automatic retry.*

States for every external action: queued -> presented -> authorized -> executing -> succeeded/failed/unknown. The authorized action may be Allow, Deny, or Cancel.

Declined, cancelled, and expired are terminal branches only when no target action was dispatched. Persist decision consumption before the external side effect.

Serialize competing transitions. After cancellation, expiry, revocation, or consumption, reject all approval replays.

Late approvals return an explicit stale result.

### Concurrent decisions from multiple phones

![First valid decision accepted by the Mac wins](site/dist/figures/race.svg)

*Arrival at the Mac ledger determines the winner, not device timestamps. The second decision is rejected even before its notification disappears.*

| Event | Authoritative result | Other devices |
| --- | --- | --- |
| First valid Allow, Deny, or resolving Cancel | Atomically consume the request | Remove actions and dismiss its notification |
| Conflicting decision arrives later | Return Already handled with the winning decision | Show the resolving device and action |
| A phone was offline | Keep the Mac terminal state | Reconcile on reconnect; reject stale approval |
| User closes a sheet | Request stays pending | No change on other devices |

A handled request is resolved, not expired. Retain the decision and outcome in the audit log.

### Crash and uncertain-outcome handling

Execution cannot be made transactionally atomic with the filesystem or arbitrary child effects. On service crash after consumption, report unknown unless the running child can be safely identified.

Never automatically retry an uncertain action. Persist only minimal operational metadata by default; no plaintext command, credential, UI snapshot, or environment in logs.

Keep pending sensitive payloads in memory; a root-authority restart cancels unconsumed requests. Other component restarts follow the matrix in section 17.

Explain uncertain outcomes in the phone and caller UI.

The root service watches each GUI dispatch permit for a bounded completion acknowledgment. Adapter death, lost IPC, or completion timeout after consumption produces unknown, including a crash before any click.

Invalidate the permit and require adapter cancellation/reconciliation before issuing another for the same dialog; never let a late adapter act with an expired permit. No automatic retry is permitted.

Expose Refresh request for user-initiated recapture after the old actor is quiescent. Test kills before and after the external side effect and acknowledgment loss.

## 7. Android biometric and credential handling

### Biometric signing keys

Create an EC P-256 biometric signing key for each (stable Mac identity, account identity) pair in Android Keystore. Bind the key ID and enrollment record to that tuple.

Show the account in the request so multiple accounts on one Mac remain distinct. Require BIOMETRIC_STRONG for every use of that biometric key, with no authentication grace period.

Use BiometricPrompt.CryptoObject and StrongBox where supported. Select and test biometric-enrollment invalidation behavior through the pairing-preservation gate in section 5; do not assume forced invalidation is the chosen policy.

No generic notification Approve action that bypasses request inspection. Notifications hide sensitive request content on the lock screen by default.

### Keep consent bound to the visible request

Bind the biometric operation to an immutable selected request. Request switching, cancellation, rotation, app backgrounding, and process death must not sign a different request or resurrect a cancelled one.

Test all transitions, including concurrent requests from multiple Macs. Apply the same immutable-selection and explicit foreground-tap rules to decisions made without biometrics.

The user confirmed that the normal request UI is unavailable while the phone is locked. Use that normal Android navigation flow; do not introduce a separate lock-screen approval surface or an extra app-unlock/biometric step for one-time decisions.

No show-over-keyguard activity is part of this design. Protect decision controls against obscured touches and overlays where the platform supports it.

Other unprivileged Android apps are untrusted; phone OS compromise and compromise of the installed Remozio app are outside the trusted-phone assumption. Evaluate a platform unlocked-device constraint on the decision key for supported Android versions without changing the stated interaction flow.

Never infer consent from notification delivery, timeout, or the selected default. Revocation removes both biometric and decision-key authority for that enrollment.

### Credential enrollment and release

![Credential release sequence](site/dist/figures/credential.svg)

*The encrypted credential is bound to the certified adapter recipient and exact request. One biometric operation remains a feasibility gate.*

Credentials needed by UI adapters are enrolled explicitly on the phone and encrypted with a separate biometric-protected Keystore key. Never extract credentials autonomously from 1Password.

Do not use clipboard sync, command-line arguments, analytics, crash reports, or disk logs for secrets. Release a credential only to the paired adapter and the exact approved unlock request using authenticated encryption and a short-lived request-bound channel.

### Feasibility gate: one biometric operation

Spike whether one BiometricPrompt CryptoObject operation can securely unlock a request-bound credential package while authenticating the response. Do not assume two separate Keystore operations share one authentication.

If the design requires a second biometric or weaker key protection, consult the user. Minimize plaintext lifetime and copies; do not promise perfect zeroization on managed runtimes.

The destination Mac necessarily sees a supplied password.

## 8. Command execution adapter

### Initial command workflow

Use `remozio run -- <command>` for privileged command approval. Transparent sudo integration is a separate experiment. This explicit frontend applies only to commands; 1Password and Little Snitch keep their normal prompts and automatic capture.

### Capture the invocation and input source

![Command execution sequence](site/dist/figures/command.svg)

*Capture comes before approval. The root service validates and consumes the decision before executing the captured argv.*

The frontend submits an argument vector and requested options. The privileged service captures and canonicalizes the effective executable, argv bytes, working directory, target UID/GID and supplementary groups, environment, and I/O mode before requesting approval.

Bind the stdin source classification and actual passed stream: caller-controlled pipe, file with available identity/path, caller-controlled TTY/PTY, or null input. Show this on the phone, including that caller-controlled content is not captured or approved byte-for-byte.

This applies to shell scripts read from stdin, data piped into privileged writers, and commands subsequently entered through a PTY. Input disclosure must not imply that stdin from a terminal is necessarily human-authored.

Streaming input remains supported; prebuffering or restricting it would require user consultation. Resolve paths once and preserve directory descriptors where supported.

Credentials and executable identity come from trusted OS queries, not caller labels. Distinguish verified process identity from caller-provided rationale.

### Render the exact invocation

Display the full executable and argument vector, working directory, target user, and effective environment changes. Include a compact verified-requester row with submitting executable path/signing identity, UID, PID/process incarnation, and available TTY/session information in the signed payload.

Capture available upstream process ancestry at submission, with clear limits: an authenticated frontend is not proof of which AI model or human invoked it. Label caller-supplied rationale and unverified ancestry as unverified; do not present a caller label as a security identity.

Use escaping that makes newlines, control characters, bidi controls, empty arguments, and non-UTF-8 bytes unambiguous. Preserve raw bytes for execution.

The main view is readable; expandable raw arguments expose the exact invocation. Do not alter or hide signed values behind a cosmetic command summary.

### Deterministic environment

Use a deterministic minimal environment with explicit requested additions. Capture it before approval; never inherit the service's ambient environment.

Preserve common workflows by specifying PATH/HOME/locale behavior and testing it. Any environment restriction that breaks a requested workflow becomes a user decision, not a silent refusal.

### Execute argv without an extra shell

Execute the captured argv directly without an extra shell. Explicit shell invocations retain their complete `-c` script as an argument.

Shell operators outside the frontend remain in the calling shell; documentation and integration tests must distinguish `remozio sudo -- cmd > file` from an explicitly approved privileged shell redirection.

### Caller lifetime and executable races

Bind request to caller/session lifetime; define interactive detach separately. Measure each targeted coding tool's timeout and cancellation behavior.

Configure integrations to allow the intended approval window, and show requester-exited or tool-timeout explicitly on the phone. Do not shorten the approval window to match a harness timeout.

An optional resumable/detached mode needs a separate user decision; it is not the default. Before execution, verify executable identity and working directory remain consistent with capture.

File identity/hash checks narrow accidental substitution but are not atomic guarantees against mutable scripts or dependencies. Spike descriptor-based execution on macOS.

If an atomic execution mechanism is unavailable, document the concrete path-execution/recheck race and present the user with the measured options before enabling that guarantee-sensitive path. Do not silently copy the executable or script into privileged storage: relocation can change argv/path behavior, relative resources, loader behavior, and signature assumptions.

The gate must deliver an explicit supported semantic contract; it cannot claim atomic entry-file binding from a hash check alone. No default fallback that changes program location or restricts programs is authorized by this plan.

Do not promise immutable transitive behavior or impose an allowlist of immutable programs without user approval.

### Process I/O and terminal outcomes

Provide streaming stdin/stdout/stderr, exit status, signal forwarding, process-group cancellation, terminal resize, and an explicit PTY mode for interactive commands. A disconnect must not silently repeat a command.

Recheck caller/session liveness through the retained verified IPC identity at the execution boundary. If it has ended before any child starts, consume the decision and report Requester exited before execution as a known no-effect terminal outcome.

If exit races after the child starts, use the started-command lifetime policy; do not claim an atomic guarantee from a liveness check. Only report no effect when the service can establish that no child started.

Define whether a started command continues or terminates as a visible per-request option; default to normal attached CLI lifetime. Denied, stale, unavailable, oversized, and requester-exited-before-execution outcomes return distinct nonzero statuses.

All admission rejections use an authenticated, submission-bound no-admission result. Bind Mac/account, submission identity and nonce, reason, and retry class. It proves that this submission created no request and dispatched no action; it is not an action outcome.

| Busy reason | Frontend behavior | Deadline result |
| --- | --- | --- |
| Update installing | Bounded automatic wait and fresh resubmission | Distinct Update installing nonzero status |
| Mac authority starting | Bounded automatic wait and fresh resubmission | Distinct Mac authority starting nonzero status |
| Update waiting | Show drain state; bounded automatic wait and fresh resubmission | Distinct Update waiting nonzero status |
| Storage unavailable | Show storage state; bounded automatic wait and fresh resubmission | Distinct Storage unavailable nonzero status |

The wait uses one caller deadline across all retries and reason changes; backoff never resets it. Preserve cancellation, caller liveness, and unconsumed stdin/PTY input. The drain or outage can last indefinitely, but the caller's wait cannot.

A busy state that clears within the deadline needs no manual rerun. Deadline exhaustion reports the specific retryable reason and available status controls; it never loops forever or reruns through sudo. This does not shorten an admitted request's approval lifetime.

Only these negotiated retry classes authorize automatic resubmission. Permanent policy rejection, unknown reason codes, lost acknowledgments, and ambiguous admission results do not. A generic retry hint is insufficient proof. GUI capture follows the same classified recovery rules in section 17 while preserving the target's age and expiry.

Manual sudo remains available; never automatically rerun a denied command through sudo.

### Feasibility gate: sudoers compatibility

Native sudoers policy compatibility is an explicit feasibility gate. A custom executor is a new elevation authority; macOS administrator membership alone does not reproduce all sudoers restrictions.

Before shipping, present the feasibility evidence to the user and select with their approval either an integration that enforces the existing applicable policy or an explicitly administered Remozio policy with a clear installation disclosure. Never claim automatic preservation of all sudo semantics.

## 9. UI request adapters

### Capture a structured assertion

![UI action sequence](site/dist/figures/ui-action.svg)

*Scope mutation and final submission share one consumed permit. Failure after mutation must not trigger an automatic retry.*

Use AXObserver and structured Accessibility attributes for known application versions. Capture the app bundle/team identity, process incarnation, dialog instance, relevant fields, permitted controls, and a semantic revision.

Do not rely on window titles or coordinates alone. Treat app-provided UI text as untrusted display data.

Sign exact captured details while clearly marking unavailable fields. Apply the command renderer's unambiguous escaping and raw-access rules to every phone-rendered field, including process names, paths, hosts, item names, captured UI text, and rationale.

Keep labels separate from values so untrusted text cannot create fake detail rows.

### Revalidate identity, semantics, and deadlines

Revalidate immediately before the action, including process identity, dialog instance, semantic fields, selected scope, and control identity. Ignore cosmetic animation and ticking countdown text.

During feasibility capture, establish whether each supported dialog has its own timeout and what it does on expiry. Bind any observable deadline and default effect to the request; a changed deadline or default is semantic, while the passage of time is not.

Show the target deadline separately from Remozio expiry, with the target's default effect and uncertainty where relevant. Do not shorten Remozio's configured window or alter target defaults.

If the target resolves itself, report Resolved by target timeout only when observed, together with its observed effect or an explicit unverified result. Otherwise report disappearance with the cause unknown, without inferring a timeout or success.

Material changes cancel the old approval and replace it with a new request. A screenshot can supplement structured details, but OCR never silently establishes execution authority.

### Compatibility and unsupported dialogs

Accessibility observation and clicking are not one atomic transaction. Test races deliberately; if the target provides no stable way to associate a control with a request, record the residual risk.

Consult the user on reduced functionality or extra confirmation rather than silently disabling a useful path. Unsupported dialogs retain a visible pending notification and a manual remote-control route.

Each adapter defines a validated compatibility contract for signing identity, version range, AX structure, field meanings, controls, and action effects. A compatible update can continue within that contract; a new version number alone need not break support.

An unrecognized version or structure outside that contract cannot inherit one-time classification. Continue read-only detection and report the exact compatibility gap through the existing unsupported-dialog route.

Do not offer biometrics as a way to override unknown target/action semantics. Any broader fallback or loss of a previously supported workflow requires a concrete compatibility investigation and user consultation before changing product policy.

When a request expires but its dialog remains, retain an Expired entry with Refresh request. That action recaptures the current dialog and creates a fresh challenge for the existing normal approval flow.

It does not authorize an action, reuse a prior biometric, or automatically resend notifications indefinitely. If the dialog disappeared, report that instead.

Rate-limit refresh without silently hiding the still-pending state.

### Target the secure field

Never send a password through generic keyboard focus. Prototype direct targeting of the verified secure field.

If the platform only permits synthetic typing, stop automatic credential delivery for that adapter and present the concrete risk/UX choice to the user. Do not disable SIP or protected system input to force support.

Every external UI action, including a target-app Deny or Cancel, requires a one-time ledger permit and the same identity, dialog, and scope revalidation. Declining the Remozio request itself performs no target-app click.

Test Deny/Cancel against reordered and replaced dialogs.

### Classify the actual one-time effect

Apply the confirmed action/scope policy in section 1 to both Allow and Deny. Cancel, deny-once, and allow-once bypass biometrics only when their actual effect is confined to the current request.

Validate this effect for each supported app/version; a button named Once or Cancel is not sufficient evidence. A reusable timed/session rule counts as rule setting and requires biometrics.

For Little Snitch, the current request means only the connection attempt or fixed set of attempts captured in that alert. Any authorization that can match a later attempt is reusable, however short its lifetime or whether the app calls it internal bookkeeping.

Validate a second connection attempt and attempts that arrive while the alert is open; they must not inherit a biometric-free decision.

### Scope mutation is part of one attempt

Capture supported action/scope choices in the immutable request. A phone-selected scope is bound in the signed decision and must match a captured supported choice.

Consume the one-time permit before any dialog mutation. Scope selection, re-reading the selected scope, and the final click form one bounded attempt under that permit.

If scope selection changes the dialog and any later check or step fails, report unknown and do not retry or restore controls automatically. Mark a refreshed request when its observed default may have been changed by the earlier Remozio attempt.

The phone decision still authorizes only the chosen final action and scope, never arbitrary UI operations. Unexpected semantic changes invalidate the request; they cannot upgrade a decision to Forever.

Do not silently fall back from Once to a reusable rule, or from a reusable rule to Once. If an app cannot provide true one-time behavior, report that feasibility result and consult the user before changing the intended UX.

### Confirmed allow-once tradeoff

Allow-once without biometrics is confirmed for Little Snitch. Its authority covers only the captured current request, not command execution or credential access.

An operator with access to the unlocked phone app can make these one-time decisions without proving their identity again. This is the chosen UX tradeoff; no automatic or background approvals are permitted.

### 1Password

#### Separate vault unlock from key access

Model vault unlock and item/key access as separate operations. Unlock approval explicitly describes that the vault becomes available under its configured lock policy; it does not promise item-only access.

Show account/vault identity only when exposed. Access approvals show requesting application, operation, item or SSH key, and any destination shown by the dialog.

Clearly label missing destination or item data.

#### Feasibility gate: 1Password fixtures

Capture real AX trees in a user-authorized feasibility session before committing to selectors. Do not change 1Password lock settings to simplify automation.

The app version, locked/unlocked state, SSH agent prompts, CLI integration, password fallback, and failed unlock all need fixture coverage. Prefer existing app authentication paths where compatible; credential injection is a separate capability, not assumed public API support.

### Little Snitch

#### Capture and bind the firewall rule

Show executable/process identity, destination host/IP, port, protocol, direction, and proposed rule duration/scope. Both Allow and Deny decisions bind their exact scope.

Reflect existing dialog defaults initially; do not silently change a user's permanent rule to temporary or vice versa. Phone scope changes must be reflected in the signed action and applied to the same dialog before confirmation.

Both the root service and adapter reject a biometric-free decision if the selected effect is a reusable rule, regardless of the visible button label.

#### Feasibility gate: Little Snitch scripting

The GUI scripting setting is a broad permission. Installation explains the tradeoff and verifies the setting; it does not change it covertly.

Permit the approval transport before depending on it for firewall prompts, with a narrow user-approved rule. Test multiple queued alerts, DNS/IP changes, process restart, and dialog reordering.

Report applied scope where observable; otherwise distinguish action submitted from rule verified.

## 10. Delivery stages and acceptance gates

1. Feasibility: inspect supported macOS APIs, privileged installation/policy options, 1Password/Little Snitch AX trees, Android background delivery, and single-biometric credential release. Deliver concrete compatibility evidence and user decisions.
2. Protocol harness: fake Mac/phone peers, enrollment, canonical vectors, replay/expiry/revocation tests, durable consumption and crash injection. No real elevation or credentials.
3. Command vertical slice: signed Mac helper plus Android approval, full argv rendering, trusted capture, basic process I/O. Run in a disposable test account or VM with harmless commands.
4. Coding workflow: real caller integrations, PTYs, cancellation, reconnect, concurrent request inbox, useful errors and latency measurements. No blanket sudo override.
5. Little Snitch adapter: notifications and structured read-only detection first, then bound actions after fixture and race tests.
6. 1Password adapter: read-only detection first, access approvals next, credential-based unlock last after the biometric and field-targeting gates pass.
7. Hardening: signed updates, privilege boundary review, pairing recovery, uninstall, fault injection, real-device soak tests, and independent implementation review.
8. Optional ADB deployment bridge: validate Android local port discovery and foreground service behavior, then implement and test the separately authenticated byte stream in section 19. This stage does not delay the approval product.

### First implementation experiments

| Order | Experiment | Required evidence |
| --- | --- | --- |
| 1 | One-app installation, service registration, XPC, and update replacement | Signed arm64 test bundle; login, lock, logout, restart, uninstall, and peer-rejection results |
| 2 | Protected key custody and durable authority state | Key availability by session state; non-exportability, crash, clone, and rollback results |
| 3 | Prompt adapters and presence | Authorized 1Password/Little Snitch fixtures; Chrome Remote Desktop and Screen Sharing observations |
| 4 | Per-Mac tunnel and FCM delivery | Independent Macs, background Pixel delivery, credential scope, and no separate host dependency |
| 5 | ADB over Wi-Fi and mobile data | Listener exposure, enabled-pair wake/reconnect, self-update, Stop, and interrupted-operation results |
| 6 | Setup export and application updates | Independent import, wrong-password/tamper rejection, key separation, signed APK update, and retained pairing results |

Use disposable test identities and explicit test operations. Capture measured results and limitations beside each gate before selecting implementation libraries. Do not change normal device permissions or debugging settings merely to inspect feasibility without the required user authorization.

### All adapters are in scope

Stages 5 and 6 are part of the intended product, not optional features to discard after sudo works. Feasibility failures require a user-visible decision.

### Acceptance: Authority and enrollment

- wrong key/Mac/account rejected
- rogue local client cannot enroll keys
- revocation during an open prompt
- concurrent decisions from two enrolled phones consume once and synchronize the result
- revoking one phone leaves pending requests available to another and rejects signatures from the removed key
- replacement removes old-phone authority
- same-user identity-key access and adapter impersonation fail
- a second local account cannot reuse enrollment authority
- cancel, true deny-once, and true Little Snitch allow-once work without access to the biometric key
- both verifiers reject decision-key authority for commands, 1Password access/unlock, credentials, and enrollment
- same-user code injection and bundle modification fail
- request controls are not available over the keyguard
- biometric changes preserve pairing where supported and otherwise expose action-specific recovery
- an existing decision key cannot replace biometric or credential authority
- reusable Allow/Deny rules, including temporary/session rules, reject the decision key
- relay compromise cannot mint a one-time decision
- remote administrator recovery works without the old phone
- pre-login transport credentials remain inaccessible to an interactive account

### Acceptance: Action scope and biometrics

- altered command/environment/scope rejected
- unprivileged user cannot obtain admin scope
- later connection attempts do not inherit once-decisions
- a once-decision cannot move to another alert
- unknown compatibility contracts cannot inherit one-time classification
- scope-mutation failure is unknown and refresh labels a potentially modified default
- Forever defaults require biometrics
- scope substitution and Once-to-Forever races are rejected

### Acceptance: Request lifetime and concurrent decisions

- duplicate/expired/cancelled approval rejected
- no execution on service restart
- uncertain action never repeated
- changed/disappearing dialogs
- observed target timeouts distinguish verified effects from unknown disappearance
- caller exit around the execution boundary distinguishes no child started from started/unknown outcomes
- coding-tool timeout is visible
- sleep longer than the approval lifetime expires the request
- stale UI Deny/Cancel clicks nothing
- a consumed GUI permit with no acknowledgment reaches unknown and cannot act later
- an expired visible dialog can be refreshed without replay

### Acceptance: Capture and UI integrity

- full command render round-trip
- no secret logging
- no password in another app
- oversized argv/environment captures report Request too large without truncation
- command approval works without Aqua login
- obscured taps do not authorize a decision
- malicious UI strings cannot spoof labels or fields
- stdin source/mode substitution fails and uncaptured input is disclosed

### Acceptance: Platform operation and end-to-end results

- phone locked/Doze/offline/reboot
- Mac sleep/logout/reboot
- end-to-end success and denial for each adapter
- relay changes to payload/digest fail
- credential recipient substitution fails

### Measure real workflow latency

Document measured notification latency and approval latency under local and remote connectivity. Product acceptance requires practical remote use without repeated confirmations or unexplained silent stalls.

Specific performance targets will follow device measurements.

### Acceptance: Several Macs in one app

- Pair two Macs with one Android app, and enroll a second Android device with only one Mac.
- Receive simultaneous requests, including equal request IDs and duplicate display names, without merging them.
- Open a notification from another Mac while a filter is active; keep any in-progress decision bound to its original request.
- Change routing, revoke an enrollment, rotate push tokens, and disconnect one Mac without disrupting the others.
- Reject a decision or credential release replayed against another Mac/account.
- Show combined audit history while preserving independent sync cursors, clock differences, and offline states.
- Keep supported Macs usable when another Mac requires a protocol update.
- Confirm approval enrollment cannot create or extend an ADB grant.

## 11. Review protocol

### Independent review procedure

Run independent reviews with claude-code/claude-opus-5-5, kimi-code/k3-256k, and zai/glm-5.3. Preserve model identity, input revision/hash, terminal status, report path, and findings.

Resolve concrete implementation-plan defects, then ask all three reviewers to inspect the same revised plan. Do not replace unavailable models silently.

### What convergence does and does not mean

Maintain reviews/REVIEW-LOG.md with accepted fixes, reasoned disagreements, pending UX decisions, and outstanding evidence gates. Convergence means all three requested models report no substantive unresolved planning findings covering the same final revision.

If a provider exhausts its output limit, use bounded overlapping scopes without changing the requested model. Preserve excerpt hashes and coverage; an empty report never counts as success.

Unchanged scopes may retain prior reports only after exact text comparison against the final revision. Re-review changed scopes and their affected cross-section interactions.

It does not mean platform feasibility is already proven or the implementation is secure. This task creates a plan only; no security settings, apps, or privileged components are installed.

## 12. Audit log and app history

### Read history in the app

Remozio must keep an audit log accessible from the Android app. Audit log is a primary navigation destination on compact and expanded layouts.

Reading retained history needs no extra biometric prompt. Show a newest-first list, filters for Mac, request type, and outcome, and a detail timeline for each request.

Keep unknown outcomes visible. History never offers replay or approval of an old request.

The combined history labels every event with its Mac/account. Sync cursors, authority epochs, sequence numbers, retention gaps, and offline status remain separate for each enrollment.

Do not infer a global event order from clocks on different Macs. Each request timeline preserves its issuing Mac’s authoritative order.

### Events to retain

Record request creation, authenticated decisions accepted or rejected, consumption, dispatch, verified results, expiry, cancellation, and unknown outcomes. Also record pairing, revocation, recovery, update scheduling, activation, update-caused interruption, and optional ADB bridge session start, stop, peer, and stop-reason events. Bridge events contain metadata only, never ADB frames, APK bytes, shell commands, or device data.

Individual rejection events require a verifiable signature from a current or retained former enrollment key for that Mac/account. Do not allocate durable per-packet events for unauthenticated traffic. Use bounded, rate-limited aggregate counters without attacker-controlled labels or payloads.

Rate-limit authenticated rejection telemetry too, including repeated replays from revoked keys. Aggregate repeats with explicit dropped-event counts; bound counters, cardinality, and write frequency. A stolen old key must not turn rejections into unbounded disk writes.

Reserve capacity and priority for consumption and outcome records. Admission of a real request reserves its bounded lifecycle budget; rejection floods cannot spend that reserve or force early pruning of legitimate history. If essential recording cannot be guaranteed, return the authenticated, submission-bound Storage unavailable no-admission result defined in section 8, before dispatch. Test sustained forged and revoked-key traffic alongside local approvals.

Distinguish a phone decision from Mac acceptance, an attempted action from its observed effect, and a Remozio dismissal from target cancellation. A cancelled biometric prompt is not a denial.

Dismissal leaves the pending request unchanged.

### Event schema

Each event has a stable ID, request ID where applicable, journal epoch and per-epoch sequence, Mac/account identity, event time and receipt time, request category, selected action and scope, deciding phone identity, authentication class, and outcome evidence. Use structured reason codes.

Audit action and scope are closed metadata classes, not the full signed action payload. Record action kind, once/session/timed/forever lifetime class, and host/domain/any scope class where applicable.

Do not store actual hosts, addresses, ports, item identities, UI labels, or other bound target values in those fields. Full scope values exist only in the memory-resident request.

Map unrecognized values to a defined metadata category; never copy raw provider or caller strings into audit enums. The history distinguishes one-time and reusable decisions without retaining their sensitive targets.

Show missing or unverified fields explicitly. Do not record biometric samples or claim the biometric proves a particular person acted.

Order each Mac's events by journal epoch and sequence. Show explicit discontinuities between epochs; label clock differences across devices.

### Atomic journal writes

![Audit write and synchronization sequence](site/dist/figures/audit.svg)

*Consumption and the initial audit event commit together. The phone receives metadata through authenticated, account-scoped sync.*

The root service owns the durable authoritative journal and consumption ledger. Enforce one live writer with an OS-held exclusive process lock in protected storage for the service lifetime, plus transactional compare-and-set on retained request state.

All service instances and maintenance/recovery paths must honor that exclusion. Lock files alone are not ownership proof. Validate stable lock identity across installation, restore, and replacement; inability to establish exclusive ownership keeps new action admission closed.

Commit request consumption and the corresponding audit event in one local transaction before dispatch.

Maintain a protected local continuity checkpoint and recovery-required marker outside the replaceable journal. Bind the checkpoint to the ledger and journal head, including consumption and outcome boundaries. It supports crash recovery and detects partial replacement; it is not proof against whole-backup rollback.

Before dispatch, the journal transaction and its checkpoint complete a crash-consistent commit protocol. Define deterministic recovery for every interrupted phase and test power loss at each boundary. A partial commit never permits dispatch.

Validate writer ownership, the checkpoint, the marker, and retained ledger evidence at startup before admission and epoch creation. A protocol-resolvable pre-dispatch interruption rolls back or forward automatically and records no action dispatched. An interruption that may follow dispatch preserves the consumption and records Unknown unless the effect can be verified. Neither case retries the action.

Only a genuine mismatch or an unresolvable commit state sets recovery-required. Close admission immediately on detection and persist that marker. Failure to write it leaves admission closed in the live process; startup must independently validate the checkpoint and commit state. A restart alone is not permission to skip recovery checks.

Recovery first proves exclusive ownership and current authority-state continuity under section 16. If only audit/ledger history is lost, recover automatically into a fresh epoch with a labelled discontinuity. Preserve all retained evidence and mark unresolved outcomes Unknown. For records that cannot be enumerated, show a history-loss interval rather than inventing individual events. Historical loss alone does not require administrator repair.

Do not resurrect any request from restored history. Pending payloads are memory-only, restarts cancel them, and new requests receive fresh challenges. During live recovery, retire pending requests and reconcile or quiesce outstanding permits before reopening admission. Already-dispatched work is not killed or retried.

If exclusive ownership or current authority has a genuine continuity mismatch or an unresolvable state, keep admission closed and use the existing repair path. This is distinct from protocol-resolvable interruption or history-only loss.

A transient witness read/write or storage failure, including the epoch-creation write, keeps admission closed in a retryable starting state. Retry automatically with bounded backoff. Show “Mac authority starting / storage unavailable” to the caller and in phone connection health; that status grants no action authority.

A timeout or temporary unavailability alone is not evidence of rollback and must not trigger administrator recovery. Preserve cancellation and keep input unconsumed while admission is closed. Surface persistent unavailability honestly without silently requiring GUI login.

When the authority can authenticate a submission-bound response confirming that nothing was admitted, return Mac authority starting as a retryable no-admission result. Apply section 17's bounded wait, command resubmission, and GUI recapture rules to it. A generic health indication or lost response is not this proof and cannot trigger replay.

Anchor the current checkpoint to the combined witness at epoch creation and authenticated trust, floor, or replay-control changes. Per-action journal commits use the local crash protocol; they do not require per-action witness writes. Later audit-only truncation may be detectable only from retained checkpoints or phone receipts. Never claim that a prior-head link proves no later events existed.

Reconcile outcomes after restart without retrying the action. Derive the audit state from the existing durable consumption ledger; do not create a second execution authority.

If the required journal transaction fails, report that no new action was dispatched. A lost outcome write must remain unknown rather than imply success.

Audit availability failures must be visible in the app and caller.

### Sync and offline cache

Expose paginated, read-only history over the existing authenticated channel, scoped to the enrolled account and authorized Macs. A revoked phone cannot fetch new history.

The phone caches encrypted records for offline reading, with the last sync time and visible gaps. Deduplicate within the Mac/account and journal epoch by event ID. Resume from an authenticated cursor bound to that epoch and sequence.

Signed batches bind the Mac, account, journal epoch, epoch-creation trust generation, sequence range, and retention boundary. Detect missing or changed records within retained history.

The epoch-creation generation is immutable descriptive metadata from the authenticated epoch header. Every batch for that epoch must carry the same value; phones reject conflicts. A normal revocation or floor commit does not rewrite it or reset history.

This field is not a claim about current authority or policy. Verify current channel and authority identity separately under the normal trust rules. Audit metadata cannot enroll a phone, lower a floor, or override known trust state.

A journal epoch is bound to the combined trust generation at its creation. Normal trust updates do not reset history. Generate a fresh cryptographically random epoch before the first journal write on every root-service start, and after journal replacement or recovery.

Never resume sequence allocation in a persisted older epoch. The exclusive live writer owns its epoch and monotonically increasing sequence; verify its expected head before each append.

On a head mismatch, stop appending and close new action admission. Use the classified recovery protocol above before consuming another request or creating a fresh epoch.

Automatic recovery requires exclusive ownership and current authority continuity, not proof that no audit record was lost. Historical gaps remain visible; missing outcome evidence remains Unknown.

After validated recovery, start a fresh epoch with a discontinuity marker. Test duplicate service starts, lock replacement, concurrent maintenance, live journal replacement, and head mismatch before consumption and after dispatch. Repeat a head mismatch followed by a crash, launchd restart, reboot, and whole-backup restoration. Verify recovery checks, accurate loss labels, automatic recovery for history-only loss, and no action replay.

This prevents sequence reuse even when a backup restore leaves enrollment and security policy unchanged. It does not rely on the trust witness detecting an audit-only rollback.

Keep old epochs and their records subject to the chosen retention policy. Link a new epoch to the authenticated prior head when available. A normal restart alone is not evidence of rollback; label the cause accurately and do not invent missing history.

When an authenticated phone cursor or retained batch receipt exceeds or conflicts with the Mac's retained history, reconcile read-only. Preserve the phone's cached evidence and mark the gap as a possible journal rollback or loss. A phone report cannot rewrite Mac history, change trust, or authorize an action.

Each page request names its epoch; unavailable old history returns an explicit boundary or gap plus the current epoch. Reconnect then syncs the new epoch without waiting for old sequence numbers to reappear.

Return an authenticated epoch transition with its restart, replacement, or recovery reason and the known prior boundary. A cursor from an older epoch cannot select records from the new epoch, even when sequence numbers repeat.

Phones preserve cached old-epoch records. Show “History discontinuity after Mac restore” for confirmed restoration, and a possible-loss label when the cause is unknown. Fetch the new epoch from its available boundary; never overwrite conflicting old history or stay stalled behind an obsolete cursor.

Do not claim a continuous chain when prior evidence is unavailable. Authority-key changes must pass the authenticated migration or recovery flow before phones trust new-epoch batches.

Test a cursor ahead of a restored journal, repeated sequence numbers, offline phones, missing prior records, and a recovery followed by another crash. Audit epochs cannot authorize or replay actions.

This cannot prove integrity after a root compromise and cannot remotely erase an offline revoked phone.

### Sensitive data exclusions

Keep only structured metadata by default. Do not persist raw commands, arguments, environment values, passwords, key material, item names, destinations, UI text, screenshots, or command output in either journal or cache.

Sensitive pending payloads remain memory-only as already specified. The detail view explains that full request contents were not retained.

Diagnostic and crash logs follow the same exclusion rules. A future opt-in for richer history needs a separate user decision.

### Retention and replay safety

Retention must be configurable and shown in the app before pruning begins. Select its duration and storage budget during implementation with the user; do not silently discard history.

Record retained sequence boundaries and make expiry or missing history explicit. History retention must never delete active requests or weaken the consumption ledger and replay protection.

Pairing and revocation records required for current trust remain independent of history retention.

### Audit acceptance cases

Acceptance: verify each lifecycle event, one-time and reusable decisions, cancellation versus dismissal, concurrent phone decisions, crash recovery, journal write failure, uncertain outcomes, offline history, cursor gaps, deduplication, revoked-phone access, and retention boundaries. Confirm that seeded secrets never occur in the journal, cache, exports, or diagnostic logs.

Test power loss at every commit phase: resolvable interruptions recover automatically without an administrator prompt. Test history-only loss separately from authority rollback.

Test compact navigation, expanded navigation, filters, and readable detail timelines. No audit screen may authorize or repeat an action.

## 13. Request age and target expiry

### Age begins at first observation

Show how long ago each request appeared in the inbox, notification, and request sheet or dialog. Measure from the Mac adapter's first observation of that dialog, not phone delivery or opening.

Label this as “First seen 24 seconds ago” when the actual appearance time is unavailable. Preserve the original observation time across reconnects and recapture of the same dialog.

A genuinely new dialog has its own identity and age.

### Estimated 1Password lifetime

For supported 1Password prompts, show an estimated lifetime of about 60 seconds, based initially on the user's observation. Validate this separately for each prompt type and supported version.

Show “Usually expires after about 60 seconds” and an estimated remaining time. Do not treat that duration as a universal 1Password guarantee.

When detection started late, explain that the prompt may expire sooner. If the estimate elapses while the prompt remains, show “Estimated expiry passed · checking Mac”; an estimate alone neither proves disappearance nor changes execution authority.

### Two clocks and two deadlines

Keep target expiry separate from Remozio authorization expiry. The Mac's existing monotonic deadline remains authoritative.

Send authenticated age, deadline, observation, and status information bound to the request identity. Estimate elapsed time on the phone with a monotonic clock, include delivery delay where measurable, and label stale or uncertain timing.

Do not assume synchronized wall clocks. Recompute on resume and reconnect, including time spent asleep.

A countdown must not reset when the app opens, the window changes, or the connection recovers.

### Terminal status and notification updates

![Expiry decision flow](site/dist/figures/expiry.svg)

*Elapsed estimates, observed timeout, and disappearance are different facts. The UI must not merge them into one result.*

The Mac adapter observes target disappearance and timeout events and sends a terminal status promptly. Show “Expired” when Remozio expiry or a target timeout is established before dispatch.

Show “No longer available · reason unknown” when only disappearance is known. Remove all approval controls from confirmed terminal requests and update their notifications.

Retain the request in history, with first-seen time, end time, elapsed age, and the expiry source or unknown reason. Do not present disappearance after dispatch as harmless expiry: retain the existing confirmed-result or unknown-outcome rules.

### Refresh versus a new original operation

If 1Password's prompt has gone, explain that the user must trigger the original operation again. Do not offer Refresh as though Remozio can recreate it.

If a dialog remains after Remozio expiry, keep the existing Refresh request action for a new challenge. That action preserves the dialog's age and target deadline.

Neither refresh nor a late biometric result can revive expired authority. Revalidate the dialog and deadline immediately before any action, even while a biometric prompt was open.

### Accessible timing presentation

Update visible age without moving controls or announcing every second to TalkBack. Announce meaningful status transitions once.

The prototype's Expired scene for 1Password represents an observed target timeout, not an inferred result from the estimated countdown.

### Timing acceptance cases

Acceptance: delayed delivery; late adapter attachment; timeout during biometric authentication; a disappearing dialog; target remaining after the estimate; Mac and phone sleep; reconnect and clock skew; recapture of the same dialog; a new dialog after retry; and late decisions racing expiry. Verify notifications and audit records agree with the authoritative status and no expired control can dispatch an action.

## 14. Mac activity and phone routing

### Presence changes delivery only

Route requests to the phone only when the Mac is not actively being used, unless the user selects a manual routing mode. Detection and request capture continue while the Mac is active.

Keep those requests local without sending an actionable request or notification to the phone. This routing preference does not approve, deny, cancel, or extend any request.

Audit history remains available in the app under the existing access policy.

### Routing modes on Mac and Android

| Mode | New request destination | Reason shown |
| --- | --- | --- |
| Present | Local Mac | Manual local override |
| Away | Registered phones | Manual phone override |
| Automatic: active workspace | Local Mac | Recent qualifying activity |
| Automatic: usable remote desktop session | Local Mac desktop, visible remotely | Remote desktop connected |
| Automatic: locked, dark, or idle without remote presence | Registered phones | The triggering observation |

Provide three menu bar modes: Automatic, Present (keep requests here), and Away (send requests to phone). Manual modes override automatic presence detection until the user changes the mode.

Show the current mode, effective destination, and reason in the menu, plus a clear return to Automatic. Persist the selected mode across app restart.

Make the override visible in the Android device status too. An override controls delivery only; it never grants authorization or bypasses biometrics.

No extra biometric prompt is needed to change routing locally.

Enrolled phones can select only Away from each Mac’s authenticated device status in the Android app. Keep Set Away reachable even when approval notifications are suppressed.

Automatic and Present are available only through the Mac’s local control surface, including when operated through remote desktop. Android shows these modes as status only, with no reset-to-Automatic control.

Away explicitly marks the selected Mac/account as AFK for routing. It sends eligible requests to registered phones even while a remote desktop session remains connected. Returning to Automatic from the Mac resumes presence detection.

Use a distinct routing-control purpose under the enrolled decision key. Bind Mac/account, phone identity, requested mode, operation ID, fresh challenge, expiry, and the expected routing revision.

The root verifies current enrollment and purpose, rejects every phone-originated mode except Away, then applies the change atomically against that revision. Return the current mode on conflict; do not silently overwrite a concurrent local or phone choice.

Acknowledge the resulting revision and synchronize it to the Mac menu and other phones. Retried operation IDs return the same result; stale or replayed controls cannot change the mode again.

Show the selected Mac/account and desired mode on the existing control surface. The Set Away tap needs no additional biometric or confirmation dialog because it grants no action authority.

Manual modes remain persistent until explicitly changed. Do not impose automatic expiry to solve remote recovery. Offline or revoked phones cannot change routing. For an unreachable Mac, show the last known status and an unavailable control. Do not queue a mode change or show success before the Mac acknowledges it.

Routing controls cannot modify enrollment, request scope, expiry, biometric policy, or action state. They only select delivery under the existing pending-handoff rules.

### Automatic routing conditions

In Automatic, first check for a fresh, usable remote desktop session for the target Mac/account. Such a session counts as presence, including reading without recent keyboard or pointer input.

Otherwise, route immediately when the account session is locked, no usable display is awake, or all usable displays are known to be effectively dark. With an awake local desktop, route after a configurable period without qualifying user activity.

The default local idle interval is two minutes, configurable per Mac. Show the effective interval and any device override in settings.

Do not extend a short-lived 1Password prompt to compensate for the delay. If it expires while kept local, record expiry without sending a stale approval later.

### Multiple displays and brightness

Treat multiple displays as one usable workspace: zero brightness on the internal panel alone does not mean absence when an external display is active. Lid-closed use with an active external screen can remain local.

A readable zero-brightness signal counts as dark for that display. Unsupported brightness queries are unknown, not zero.

Validate supported public mechanisms per display and macOS version before promising brightness detection. Do not install private display hooks or request broader privileges silently.

### Remote desktop use counts as presence

Chrome Remote Desktop, macOS Screen Sharing, and comparable interactive desktop tools must count as presence when their connected session can show the target account’s desktop. Physical display sleep or zero brightness does not make that remote workspace absent.

A connected usable session keeps Automatic local, so its dialogs appear through the remote desktop. Local physical lock status alone must not override a separately usable remote session. A session showing only a lock screen does not establish access to the account’s dialogs.

Remote keyboard and pointer activity count as human activity. Do not reject activity merely because it is injected rather than produced by local hardware.

A running remote-control service, listener, stale connection record, SSH shell, or background automation does not establish remote desktop presence. Remozio’s own actions must not renew activity.

Validate session detection for Chrome Remote Desktop and macOS Screen Sharing during feasibility. Document supported providers, observable connection state, target-account association, freshness, and disconnect detection. Do not claim reliable provider coverage before those checks pass.

For an unsupported provider, use available qualifying activity signals and show any detection limitation. Keep Present and Away available on the Mac, and Away in Android, as manual overrides. Do not silently install private hooks or request broader privileges.

After disconnect or loss of freshness, reevaluate the local display, lock, and idle signals. Do not let a disconnected remote session keep the Mac active indefinitely.

Collect coarse connection and activity state only. Do not record keystrokes, pointer paths, application content, or continuous activity traces.

### Stale or unavailable signals

Use a small per-account routing state machine with explicit signal freshness. Do not infer active use from a running process or an unlocked session alone.

If presence data becomes unavailable, show an Unknown reason and retain the last known routing state briefly while reconnecting. If it cannot be recovered, route to the phone to avoid silently stranding requests; label the degraded detection.

This fallback does not authorize anything. Confirm signal support and choose the bounded grace interval during feasibility.

Without a fresh usable remote desktop session, a known locked or dark state overrides a recent activity timestamp in Automatic.

### Handoff of pending requests

Reevaluate pending local requests when effective presence changes, including remote disconnect, local idle, lock, or loss of usable displays. Forward only requests whose authority and target are still valid.

Preserve their request identities, original ages, deadlines, and audit links. Returning to active use suppresses new phone notifications.

Do not abruptly close a request the user is already reviewing or invalidate its biometric operation solely because presence changed. An already delivered valid request can still be resolved from either authorized surface; the first decision consumes it once.

Synchronize the result and withdraw stale notifications. Debounce noisy display/activity changes and never repeatedly notify for the same request because routing oscillates.

### Feasibility gate: local command approval

Existing 1Password and Little Snitch dialogs remain the local interaction path. Commands submitted through the Remozio frontend need a concrete local approval surface while phone routing is suppressed.

Validate a native Mac authentication path with an equivalent per-request authorization contract; it must never auto-execute or silently rerun through sudo. Present any added authorization mechanism for independent security review.

Until that path is implemented, the caller must show the routing reason and an explicit way to select Away; it must not wait without explanation. Resolve this local-command path before claiming activity-aware routing is complete.

### Coarse audit reasons

Record routing decisions and their coarse reason alongside each request: automatic active, remote desktop connected, automatic idle, locked, display unavailable/dark, manual local, manual phone, or detector unavailable. Record mode changes and meaningful request handoffs, not every input event.

Include the selected idle interval and whether the observation was stale. Phone notifications and the Mac menu must explain where the request is waiting.

### Routing acceptance cases

Acceptance: active typing, idle threshold, lock/unlock, display sleep/wake, zero brightness, external monitors, lid-closed use, remote input, unavailable brightness, detector restart, multiple accounts, manual override persistence, and return to Automatic.

Verify pending handoff preserves age and expiry; presence changes during biometrics do not cancel user intent; racing local/phone decisions consume once; expired local requests never produce actionable notifications; and audit entries do not contain raw activity data.

Test phone recovery from a persistent Present override by selecting Away. Reject phone-originated Automatic or Present controls at the Mac authority, including correctly signed attempts. Test local/phone mode-change races, replayed controls, revoked keys, offline status, and revision reconciliation without extra biometrics.

Test Chrome Remote Desktop and Screen Sharing with remote input, connected reading, sleeping physical displays, and zero brightness. Verify account association, lock-screen-only sessions, disconnect, stale signals, and a running service without a client.

Choose Away from Android during a connected remote session. Verify immediate handoff of valid pending requests, persistence, synchronization to other devices, and return to Automatic from the Mac. Confirm that another Mac’s routing remains unchanged.

## 15. Required push delivery

### Required notification service

Push is part of the initial product, not an optional reliability enhancement. Use a notification gateway with authenticated Mac submissions and per-phone delivery registrations.

FCM is the proposed Android provider; validate device support and select an alternative with the user if needed. Do not require an always-running Android foreground service just to receive approval notifications.

### Bind and rotate registration tokens

Bind registration tokens to an existing enrolled phone identity through authenticated registration. Support token rotation, multiple phones, invalid-token removal, reinstallation, and revocation.

A push token grants no access to request details or signing authority. Never log tokens or put them in caller-visible diagnostics.

Revocation immediately stops Mac-authorized fetches and new wake submissions for that phone. Gateway suppression takes effect when its removal update is acknowledged; show pending remote cleanup until then.

Already queued provider pushes may still arrive. Their payloads remain opaque, and a revoked phone cannot fetch details or revive authority.

### Payload privacy

![Notification and request-fetch sequence](site/dist/figures/push.svg)

*The push provider delivers an opaque hint. The phone fetches authoritative state and verifies the request before showing actions.*

Keep the gateway outside the approval trust boundary. It may learn delivery metadata, but must not receive commands, UI details, passwords, or usable approval credentials.

Send an enrollment-scoped opaque routing tag with the wake identifier. Allocate a distinct random tag for each Mac/account–Android enrollment and register it through the authenticated pairing channel. The phone maps it only to that enrollment’s pinned endpoint; it contains no hostname, account name, request detail, or credential. It is a routing hint, not authority. A shared FCM token does not merge these tags.

Coalesce and rate-limit wakes per enrollment. Ignore unknown or retired tags, never follow a push-supplied endpoint, and authenticate the selected Mac before fetching its complete pending set. Failure to reach one Mac does not block others. Verify duplicate tags are rejected during enrollment and test simultaneous wakes from several Macs. Do not put request details in provider notification text.

The app can post a generic notification before fetching; show verified details once retrieved. Notification delivery never constitutes user consent.

### Fetch after wake

Provide a viable fetch path after wake. Prefer an authenticated direct connection when the paired Mac is reachable on the same LAN.

Otherwise use the internet relay, without a VPN. Both paths use the same end-to-end encrypted protocol and pinned device identities.

The relay cannot change the request or mint decisions. Cloudflare Tunnel plus Access is the first remote transport candidate; section 16 defines its boundaries.

A tunnel is a live proxy, not an offline mailbox. Add bounded encrypted storage only if a separate queue is required; do not assume Tunnel provides it.

### Recipients and first-valid-decision resolution

Respect activity-aware routing: do not send a new approval push while requests are kept local. On a transition to phone routing, send only still-valid requests with their original age and deadline.

Send to every authorized phone, deduplicate by request identity, and atomically consume the first valid decision. Notify other devices of resolution and reconcile on open or reconnect.

Resolution pushes are best effort; every approval screen fetches current authoritative state before presenting a usable action.

### Push lifetime and duplicate delivery

Bound push lifetime by request validity and any known target deadline. An estimated 1Password timeout is display guidance, not an authoritative extension or cutoff.

Discard expired or resolved requests when processing delayed pushes. Coalescing may wake the app to fetch the complete pending set, but must not silently lose independent requests.

Limit retries and notification storms without resetting request expiry. Never retry an uncertain external action as part of notification delivery.

### Delivery health and test notifications

Track gateway acceptance, provider acceptance, phone receipt, request fetch, and presentation separately where observable. Provider acceptance is not proof that the phone displayed a notification.

Show notification permission denial, delivery failure, unreachable Mac, and expired-before-delivery distinctly. Setup includes a test push to each registered phone.

The Mac menu shows delivery health and the manual fallback without claiming the phone was notified when that is unknown.

### Push acceptance cases

Acceptance: background and Doze delivery, permission denial, force-stop and restart, token rotation, revoked phones, multiple recipients, duplicate and out-of-order pushes, provider outage, LAN loss with relay recovery, short-lived 1Password prompts, and stale notifications after local resolution. Measure time from Mac observation to usable phone notification on real hardware.

The push/relay implementation and these tests are required for the first usable remote workflow.

## 16. Direct LAN and internet relay

### Choose a delivery path

The agreed transport shape has two paths: direct on the same LAN, and relayed across the internet. Neither requires a VPN.

Push remains the baseline notification mechanism in both cases. An already-connected app can receive the same event directly; deduplicate it against push delivery.

### LAN discovery is not identity

Discover candidate LAN endpoints without treating discovery, hostname, IP address, or subnet membership as trusted identity. Authenticate the enrolled Mac and phone before exchanging request data.

Discovery is only a hint. If LAN discovery is blocked, the endpoint is unreachable, or authentication fails, use the configured relay promptly.

Bound the direct-connect attempt so it does not consume most of a short 1Password window. Preserve the same request ID, expiry, and consumption state during path changes; parallel paths cannot execute twice.

### Cloudflare Tunnel endpoint

![Relay trust boundaries](site/dist/figures/relay.svg)

*HTTPS and Access terminate at the edge. Remozio payload encryption and approval verification remain between the paired endpoints.*

Evaluate Cloudflare Tunnel plus Access first for the internet path. A dedicated unprivileged Mac transport connects outbound through cloudflared to a protected application hostname. Run cloudflared under the dedicated transport account, not the interactive user.

Protect cloudflared and every executable run by that account, including loaded dependencies, configuration, registrations, and update paths. Use root-owned placement that excludes interactive-user and admin-group writes.

Launch approved absolute paths with controlled environment and dependencies. Do not use a user-writable package-manager binary, shell startup file, or self-update destination.

Record the approved third-party build and digest in the signed release inventory. Install and replace it through the protected update path; no new per-request step is required.

Apply section 3 transport-key custody to the tunnel credential and Mac push-gateway submission credential. Their files, parent directories, registrations, and provisioning paths must exclude unprivileged interactive-user access.

Keep these secrets out of command-line arguments, logs, and diagnostics. Validate same-user read and replacement attempts; a Mac-local file alone is not secure custody.

Repair supports rotating and revoking each credential independently of phone enrollment. Uninstall revokes remote tunnel and gateway access, not merely local files.

If offline uninstall cannot finish remote revocation, show the remaining cleanup explicitly and preserve a repair route. Do not claim a stolen token was revoked by deleting its local copy.

The Android app reaches that hostname over HTTPS, with WebSocket transport only if validated as needed. Expose only the Remozio approval protocol endpoint by default. When the optional section 19 bridge is enabled, a separately authenticated, narrowly routed bridge endpoint is the only additional ingress. Never expose the privileged helper, an arbitrary TCP proxy, a general shell endpoint, or other local services.

Keep the root service behind its existing authenticated local IPC boundary. Run the connector independently of the GUI session so locking or logging out does not inherently remove command transport.

Mac sleep or loss of connectivity still makes a live tunnel unavailable; show that honestly.

### Access and Remozio have different authority

Cloudflare Access is an additional access gate. It does not replace Remozio enrollment, message verification, request-bound approvals, or payload encryption.

Application-level encryption must remain end to end between the Mac authority and the enrolled phone, even where HTTPS terminates at Cloudflare. Tunnel credentials stay on the Mac; Cloudflare account API credentials never belong in the Android app.

### Feasibility gate: background Access authentication

Validate a noninteractive Access authentication mechanism compatible with background phone delivery. Per-installation service credentials are one candidate; never ship one shared service secret in the APK.

Provision credentials through the authorized pairing flow, restrict them to the Remozio application, store them securely, and support rotation and revocation. They grant transport access only.

Do not add browser login or Access reauthentication to every approval. If credential renewal requires user interaction, expose a setup or repair flow and consult the user on any recurring UX cost.

A revoked Remozio phone remains rejected by the Mac even before the Access credential revocation completes.

### Cloud management authority

A separate signed administrative setup controller on the Mac holds the Cloudflare management credential. Store it in root-only protected custody, outside the transport account and interactive-user storage.

This controller follows the protected placement, signed-generation, and update rules in sections 3 and 17. It is distinct from the approval authority and has no network listener. It makes outbound management calls only for existing administrator-authorized setup, enrollment, repair, and uninstall operations.

Authenticate its local caller and the administrator authorization for each operation. Neither a phone decision key, a push token, nor a transport credential can invoke management operations.

Separate onboarding provisioning authority from steady-state resource management. An exportable onboarding credential may create the new Mac’s tunnels, Access applications, and service tokens in the explicitly selected account or zone. Restrict its permissions to the necessary provisioning operations; it cannot be scoped exclusively to the source Mac’s existing resource IDs. Import previews that creation scope and provisions fresh resource identities.

After setup, steady-state credentials and controller operations target only that Mac’s recorded resources, with provider-enforced resource restrictions where supported. The explicit onboarding path is the only path allowed to create another Mac’s resources. Keep provisioning authority in protected setup storage; ordinary transport never receives it. Never use an unrestricted global account key.

Record actual provider permission granularity during feasibility. If Cloudflare cannot scope the credential to the intended resources, disclose the broader scope and obtain setup approval before provisioning it.

The controller enforces the exact configured account, tunnel, approval application and hostname, and local Remozio endpoint. For the optional section 19 bridge it also owns typed create, verify, rotate, disable, and uninstall operations for a separate bridge Access application and dedicated ingress. It verifies their fixed hostname, route, local endpoint, and credential scope. It exposes no generic authenticated HTTP proxy.

Reject caller-supplied alternate ingress targets, arbitrary policy changes, or unrelated token creation. Verify the resulting ingress and Access policy after each relevant mutation.

Keep management secrets out of arguments, logs, plaintext exports, Android, the unprivileged relay, and the transport process. The explicit encrypted setup export below is the only configuration-export exception. Rotate or revoke them through the same protected administrative path.

| Credential | Holder | Permitted role |
| --- | --- | --- |
| Cloudflare onboarding token | Protected Mac setup controller; explicit encrypted setup export | Create fresh resources in the selected account/zone with disclosed provisioning permissions |
| Cloudflare steady-state management token | Protected Mac setup controller | Manage this Mac’s recorded resources with provider scoping where available |
| Gateway provisioning credential | Protected Mac setup controller | Bootstrap and repair owner-scoped gateway registrations |
| Gateway mapping/control signature | Mac root authority, distinct signed purpose | Apply enrolled recipients and rotate/revoke Mac submission credentials |
| Tunnel run credential | Dedicated Mac transport account | Connect the configured tunnel; no management authority |
| Mac push-submission credential | Dedicated Mac transport account | Send opaque wakes to its enrolled recipient mapping |
| Phone Access service credential | Protected Android installation storage | Reach the designated endpoint; no enrollment or action authority |
| Push-provider credential | Dedicated Mac push service protected store | Send FCM notifications; provision during setup, never embed in the distributed app or send to Android |

The gateway's own provisioning and provider-key rotation require its operator's administrative identity. A Mac push-submission credential cannot change gateway policy or retrieve provider secrets.

If a design requires repeatedly supplying management credentials or adds recurring authorization prompts, retain a feasibility blocker and consult the user. Do not insert such steps into normal approval.


### Gateway ownership and recipient authority

Each Mac runs its own bundled notification gateway. This is a single-user homelab deployment, not a SaaS. No separate host, container, central registry, or designated push Mac is required. FCM and the selected internet tunnel remain external providers.

Enrollment, token refresh, revocation, repair, and uninstall must not need manual gateway-operator work after setup. Any design that requires it needs user consultation.

During administrator-authorized setup, the setup controller provisions one gateway registration bound to the owner, stable Mac identity, account, and pinned Mac authority key.

Bootstrap the local gateway through authenticated administrator-authorized setup. If a provisioning credential is used, keep it in the controller’s protected store and scope it to this Mac’s registration. It grants no provider or Cloudflare management rights.

The bundled gateway authenticates this provisioning operation against local administrator-authorized setup and the pinned authority. A phone, transport credential, or self-asserted Mac key cannot bootstrap or replace that trust.

After bootstrap, the Mac root authority owns the desired recipient set derived from its retained enrollment records. It signs only typed gateway-control objects under a distinct purpose.

Bind each object to owner, Mac/account, gateway identity, lifecycle epoch, monotonically increasing revision, operation ID, expiry, and exact desired mapping or credential operation.

The gateway verifies the pinned authority and owner binding, rejects stale epochs/revisions and replayed mutations, and acknowledges the applied revision. Retries of an already-applied operation return its acknowledgment without another mutation. No generic signing or cloud-management RPC is exposed.

Mapping changes, submission-credential rotation, and revocation use this control plane. The transport-held submission credential can send wakes only to the current mapping; it cannot mutate it.

#### Bind phone tokens without new approval steps

![Push token registration and authority](site/dist/figures/gateway-registration.svg)

*Only the Mac authority changes enrolled recipients. The gateway sends the challenge; the phone proves receipt through its authenticated Mac channel.*

A phone submits token changes through its existing authenticated channel. Bind them to the Mac/account, enrolled phone identity, token digest, and enrollment epoch; no biometric is needed.

Verify delivery-token possession before activating a new mapping:

1. The root validates current enrollment and signs a typed candidate-token control. Bind owner, Mac/account, phone identity, enrollment epoch, token digest, candidate ID, fresh nonce, and expiry.
2. The gateway verifies that control and retains a short-lived candidate. Only the gateway sends the opaque provider challenge to that candidate token.
3. The enrolled installation responds through its authenticated Mac channel, binding the candidate, nonce, token digest, and enrollment epoch. This response grants no action authority.
4. The root rechecks enrollment and proof, then signs the final mapping revision bound to that verified candidate. The gateway activates only the matching unexpired candidate and consumes it once.

A token string alone cannot redirect wakes. Candidate creation cannot change the active mapping; failed or expired proofs leave the prior valid mapping in place.

Revocation invalidates pending candidates for that phone epoch. A late proof cannot recreate enrollment or replace a newer token mapping.

The challenge is background protocol traffic, not an approval notification or biometric prompt. Show registration health if it cannot complete; do not fabricate delivery success.

Bound token-possession probes per authenticated enrollment. Do not expose a general send-to-token endpoint through the transport credential.

The root checks current enrollment before signing the updated mapping. Token rotation within an existing enrollment needs no new administrator prompt. Adding or replacing enrollment still follows section 5.

An offline Mac cannot authorize a new mapping; keep registration pending until it can validate the change. A phone cannot re-register itself after Mac revocation.

#### Synchronize lifecycle changes and cleanup

Persist the desired delivery revision and an idempotent control outbox with each related enrollment change. Send it through unprivileged transport, but verify control and acknowledgments end to end.

After offline delay, create a fresh authenticated control envelope from current desired state if the old envelope expired. Never replay an obsolete mapping or lower its revision to make retry succeed.

Store delivery tokens in protected operational configuration, separate from audit history. Audit records contain control outcomes and revisions, never token values or management secrets.

Local revocation takes effect immediately even when the gateway is unavailable. Retry remote cleanup and show its last acknowledged revision; do not promise atomic revocation across disconnected systems.

On a stale-revision rejection, the gateway returns an authenticated head containing its applied epoch/revision and the retained Mac-signed control receipt. Bind the response to the owner, Mac, gateway, and fresh query.

After verification, the root advances the counter to max(local, acknowledged) + 1 and signs its current trusted desired state. Do not replay an old outbox mapping with a new counter.

This repairs counter drift without operator action. A changed lifecycle epoch, revoked registration, or unverified trust state cannot be repaired by merely increasing a number.

Before counter reconciliation, validate the retained Mac-signed receipts against local trust history. A signed revocation missing locally, or a signed trust-changing revision absent from local history, indicates possible rollback.

Apply verified revocations locally before accepting any phone operation. Suspend affected phone authority and mapping publication until the existing administrator recovery resolves unknown trust changes. Gateway evidence can restrict authority; it cannot enroll a phone.

A higher gateway number alone is insufficient evidence. Require the Mac-signed receipt and its exact trust changes; otherwise report a gateway inconsistency without adopting claimed enrollment state. Known delivery-only counter drift can reconcile automatically.

Gateway phone-revocation tombstones survive mapping updates. Re-adding that phone requires fresh administrator-authorized enrollment evidence with a new phone epoch, not a replayed ordinary mapping update.

A restored backup must not silently restore revoked phone authority locally or remotely. Confirm current trust from protected non-rolled-back records or authenticated recovery evidence before enabling actions or republishing mappings.

If trust cannot be established, use the existing independently authenticated administrator recovery; preserve pairing records without treating stale authority as valid. The gateway is never an enrollment authority for local actions.

Pre-commit software rollback preserves live trust and control state; it may reconcile a stale counter but cannot restore stale enrollment records. New-epoch gateway registration is also available through authorized repair after lost state, not only after uninstall.

Uninstall revokes submission credentials and removes recipients and Mac gateway trust through the authorized control path. The gateway retains a revocation tombstone to reject delayed old control messages.

Reactivation requires a fresh administrator-authorized gateway registration with a new lifecycle epoch. Backup restoration or an old token cannot revive the removed registration.

A queued provider push may survive gateway removal. The phone still checks current Mac authority before showing actions; unavailable or revoked requests grant nothing.

### Feasibility gate: restored trust state

Prove how the trusted installation boundary and root detect restoration of security state before loading affected components or enabling authority. Ordinary files, signed snapshots, and root-only permissions do not by themselves prevent backup rollback.

Evaluate a device-bound, non-migrating installation key plus an independently protected trust-generation witness. A key alone cannot detect an older snapshot on the same Mac. Do not assume Secure Enclave provides a general monotonic counter.

Use one combined trust generation bound to the Mac/account and a commitment to authority-relevant state. Cover enrollment keys and tombstones, committed security floors, authenticated policy revisions, and the installed-generation record used by the activation boundary.

Include lifecycle epochs and replay state for gateway, routing, scheduling, enrollment, and authorization controls. Anchor the ledger/journal checkpoint at epoch creation and trust, floor, or replay-control changes as section 12 defines. Per-action audit commits and their local recovery marker use crash-consistent storage; the witness does not claim to prevent every historical audit truncation. Restoring counters must not make a previously consumed control valid again. Fresh boot challenges may invalidate ephemeral operations; they cannot restore revoked authority or lower policy.

The witness must be readable and writable from the root/daemon context before GUI login, while locked, and without an Aqua session. Record this requirement in the key-custody feasibility deliverable. A login-keychain-only mechanism is insufficient.

Fail the witness gate and consult the user if this availability cannot coexist with the required protection. Do not select a mechanism that silently requires local login or unlock. Test rebooting to the login window, submitting a command over SSH, and a transient witness fault at first start; recovery must be automatic.

Define the witness's update, atomic commit, interrupted-write recovery, backup, migration, and reinstall behavior. Validate continuity before the installation boundary loads affected code and before the root accepts phone operations. A partial commit must not enable uncommitted permissive authority. The restrictive-ahead transition below is a recognized recoverable state, not permission to treat arbitrary mismatches as valid.

Live witness faults follow the same automatic-retry policy as startup. Use a typed, durable transition intent bound to the previous witness generation, proposed generation, operation ID, and exact state change. The feasibility protocol must distinguish prepared, anchored, and finalized phases after interruption.

Apply restrictive effects immediately in the live process: revoke the phone, enforce the proposed permitted floor after activation checks, and reserve consumed replay-control IDs against duplicates. Keep affected authority closed while the anchor is pending. Persist the restrictive intent and retry the witness with bounded backoff.

Hold permissive effects, including enrollment and new recipient mappings, until the witness commit completes. Control-plane side effects that could repeat, such as scheduling or mapping publication, also wait for durable consumption. Per-request action consumption retains section 12's separate crash protocol.

A validated restrictive intent ahead of the witness rolls forward automatically after restart. It is not administrator-repair evidence. A failed or ambiguous write first reconciles the witness and intent by operation ID; retries cannot finalize a different operation or publish twice.

Report local enforcement and pending durable anchoring separately. Do not report a trust change or update activation as durably complete until the witness commits. A whole-backup restore cannot be promised to retain an operation that never reached durable completion; this limit must be visible while it is pending.

Unknown, conflicting, or unauthenticated intents remain genuine recovery failures. A transient timeout alone does not. Test revocation, floor activation, enrollment, routing, scheduling, and gateway-control consumption under witness faults, including crash before retry succeeds.

A valid enrollment record alone does not establish continuity of floors or policy. Sections 17 and 18 depend on this combined gate. Unknown continuity withholds the affected component or operation until the existing independent administrator recovery establishes trusted state.

Recovery cannot simply bless the restored snapshot. Reconstruct current floors, policy, and trust from authenticated current evidence, or retain the blocker and consult the user. A lower signed release or stale signed policy is insufficient.

When local continuity is established, normal offline and LAN operation must remain available without a gateway startup check. An unreachable gateway must not itself require administrator recovery.

If continuity cannot be established after restoration or migration, retain pairing records but withhold their action authority. Use the existing independent administrator recovery, which remains available remotely. A phone cannot approve its own restored authority.

This gate must fail explicitly if available platform mechanisms cannot distinguish a rollback while preserving offline operation. Consult the user before requiring an online check, recurring authorization, physical access, or forced re-pairing. No such UX change is approved by this plan.

Test revocation followed by restoration of a pre-revocation backup on the same Mac and during migration. The revoked phone must not act over either LAN or relay, including when the gateway is unreachable.

Restore a backup from before a floor or policy increase, then attempt to launch an older signed adapter or root authority. The activation boundary must reject it before loading privileged code. Also test stale gateway, routing, and update-scheduling controls after restoration and interrupted witness commits.

### Keep push separate from the live proxy

The notification gateway that submits push messages is a separate component. Tunnel and Access do not replace it.

Authenticate each Mac's submissions and enforce its enrolled recipient mapping. The gateway must not allow a Mac to send arbitrary pushes to unrelated registrations.

Keep push provider credentials server-side. The initial live relay can leave pending requests on the Mac rather than require a cloud mailbox; losing connectivity does not extend their lifetime.

If a cloud queue is later added, retain only expiring encrypted envelopes and keep the Mac authoritative for validity and consumption.

### Transport acceptance cases

Acceptance: same-LAN success, spoofed discovery, guest Wi-Fi isolation, mobile data, NAT, LAN-to-relay handoff, duplicate delivery across both paths, tunnel reconnect, Access token expiry/rotation/revocation, locked Mac, GUI logout, sleep, and push arrival while the Mac is unreachable. Record measured connection latency and verify there is no VPN dependency or mandatory interactive login on the normal approval path.

Attempt same-user reads and replacements of tunnel and push-submission credentials, cloudflared, other service-account executables, dependencies, configuration, and launch paths. Verify rotation, server-side revocation, and incomplete offline-uninstall reporting without destroying phone pairing.

Attempt same-user reads of management secrets and unauthorized calls to the setup controller. Reject ingress redirection, unrelated-resource mutation, token escalation, and management access using transport or phone credentials.

Test token-possession challenges, a revoked phone re-registering, transport attempts to mutate mappings, stale control revisions, offline revocation, and delayed pushes.

Verify gateway bootstrap ownership, applied-revision acknowledgments, outbox retry, credential rotation, uninstall tombstones, and rejection of old-epoch registration replay without operator intervention.

Restore a stale counter or roll back a failed migration, then revoke a phone. Reconcile the authenticated head without replaying old mappings or resurrecting revoked local authority.

Test candidate creation, challenge expiry, incorrect token proof, token-rotation races, and revocation between candidate creation and final activation. Transport credentials cannot initiate probes.

### Transport references

Transport references: [Cloudflare Tunnel](https://developers.cloudflare.com/tunnel/) and [Access service tokens](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/). These establish transport capabilities, not a verified Remozio integration.

## 17. Component identity and macOS updates

Use code signing to identify installed components, device keys to authenticate messages, and Sparkle to deliver verified macOS releases.

### What each signature proves

| Check | Evidence | Limit |
| --- | --- | --- |
| macOS code signature | Approved developer identity and intact signed code | Does not prove user consent or a trustworthy command |
| Live XPC peer validation | The connected process meets the expected component requirement | A valid signature alone does not grant every RPC |
| Enrolled device signature | The message belongs to a paired key and purpose | Does not remotely attest the running app binary |
| Sparkle archive signature | The release archive comes from the trusted release signer | Does not prove that a running helper has already updated |

Authenticate both ends of each privileged XPC connection. Require the Apple signing chain, expected Team ID, exact component signing identifier, and release entitlement policy.

Do not trust every app from the same team. Map each accepted component identity to its permitted RPCs, account, and session.

Use supported connection-bound code requirements and audit-token validation. Do not validate a caller through a PID-only lookup or a path supplied by that caller.

Validate API availability and behavior on the supported macOS 26+ Apple Silicon baseline. Do not use private APIs or silently lower the minimum identity checks.

An old, correctly signed binary may still be unsafe. On every privileged XPC request, enforce the persisted authenticated minimum build or security generation for that component role.

Obtain build identity from OS-validated signed metadata, never a peer-supplied version string. Apply this check to permit issuance, key certification, credential reception, and every privileged RPC.

Bind adapter-key certificates to that validated generation. Raising a floor invalidates older certifications and requires current peers to obtain fresh ones through authenticated IPC. Commit a new floor only through the activation transaction below.

Receiving-key access must enforce the same generation boundary; shared access-group membership alone is insufficient. Prove that an older signed build cannot use a current receiving key.

Key rotation and certification refresh must preserve device pairing. If public platform controls cannot isolate old signed builds, report a failed key-custody gate and consult before weakening protection.

Code-signing hashes change on updates. Trust the approved signing requirement and release policy rather than pinning one permanent binary hash.

### Remote provenance and separate keys

A certificate or app hash inside a network message is only a claim. Preserve the enrolled Mac authority and Android key signatures described in sections 5–7.

The Mac authority signs a validated request after authenticating its local source. Android verifies this endorsement; it does not verify a remote XPC peer directly.

Keep Apple signing credentials and Sparkle release keys outside shipped apps. Keep them separate from device, transport, enrollment, and approval keys.

Android package signing protects normal update continuity. An app-reported signing certificate is not remote attestation; no mandatory cloud attestation dependency is introduced.

### Sparkle release delivery

![Verified update and component activation](site/dist/figures/updates.svg)

*Downloading a verified app does not complete the update. The running components must reconnect, prove their identities, and pass compatibility checks.*

Use maintained Sparkle 2 for update checks and installation of the signed, notarized macOS app. Include Check for Updates in the menu bar app.

Support background checks and the user's automatic-update preference. Show the installed release, last check, available update, and actionable errors.

Serve the feed and archives over HTTPS. Verify EdDSA archive signatures and Apple code signatures; enable validation before extraction and signed feeds in a supporting Sparkle release.

Pin and test the selected Sparkle version, including its feed-signing failure and key-rotation behavior. Do not implement an unsigned fallback or run custom downloaded update scripts.

Build and sign all nested helpers correctly. Protect release signing keys separately from the feed host and relay infrastructure.

Prefer ordinary app-bundle updates with supported Apple service management only when they satisfy the protected-placement invariant in section 3. A privileged helper does not automatically require a package installer.

App-bundle convenience cannot justify launching root code from a location replaceable by an unprivileged process. Treat downloaded artifacts as untrusted until signature verification and protected activation finish.

Sparkle package updates require administrator authorization and have different rotation and installation limits. Do not select that path merely to simplify implementation.

### Activate a compatible component set

Ship a signed release manifest with app, helper, GUI adapter, transport, bundled notification gateway/push sender, tunnel client, administrative setup controller, optional bridge endpoint, and bridge command builds. Bind their service registrations and bundled executable identities to the same manifest. Include supported protocol sets, local IPC versions, storage schemas, and migration requirements. The bridge, gateway, and tunnel components follow the same protected placement, quiescence, coordinated activation, and minimum-build rules as their corresponding unprivileged transport and command roles. Include them in running-component compatibility checks and post-update startup verification. A gateway update preserves durable recipient revisions and pending control reconciliation; it cannot restore revoked recipients.

The installed bundle and running processes can differ during an update. Authenticate each running component and check its compatibility before admitting new actions.

#### Automatic activation and optional drain

Keep the existing compatible stack working while downloading and staging. Automatic root-authority activation begins only when it is already quiescent: no pending requests, active commands, or live permits.

Otherwise show Update ready and continue admitting and serving requests. Do not start an automatic admission freeze while a long-lived command or pending approval exists.

Check quiescence and close admission atomically with request creation and permit dispatch. Requests racing the brief replacement boundary receive an authenticated Update installing result bound to their submission identity, explicitly confirming no request was admitted.

For any of the four authenticated busy-state no-admission results in section 8, the command frontend transparently waits through the bounded readiness window, displays the specific state, and resubmits the original invocation. Use bounded backoff and retain cancellation and caller-lifetime checks.

Resubmission creates fresh capture and requires a fresh decision; no old approval or execution is retried. Preserve stdin/PTY semantics and do not consume input before admission.

A timeout, lost acknowledgment, or unknown admission result is not proof of rejection and never triggers automatic resubmission. Reconcile authoritative state or report the uncertainty.

If the readiness wait exceeds the configured caller deadline, return the distinct busy-state status from section 8. Reason changes never extend the deadline. This wait does not shorten any admitted request’s approval lifetime.

An optional explicit Install when current work finishes action starts the drain below. Expose it in the Mac menu and enrolled phone's authenticated device controls.

Phone install/defer/cancel-drain controls use a separate signed update-scheduling purpose under the existing decision key. Bind Mac/account, phone, action, update ID, generation, fresh challenge, expiry, and one-use state.

The root verifies current enrollment and serializes controls with activation. These controls schedule only an already-verified permitted release; they cannot supply an artifact, alter a floor, or authorize target actions.

They require no extra biometric. Show the admission effect beside the optional scheduling action; preserve the normal automatic-update path without another confirmation dialog.

Local scheduling uses authenticated local IPC. The read-only status channel carries no scheduling authority.

Stop admitting new requests to that authority and return the authenticated, submission-bound Update waiting no-admission result from section 8. Continue existing requests, normal expiry, command I/O, and GUI-permit completion.

Activate only after pending requests, active commands, and live GUI permits reach their normal safe boundaries. Do not dispatch new work while replacement is in progress.

Show what blocks the update, its age, and whether it fixes a security issue in the Mac menu and authenticated phone status. Offer defer or cancel-update-drain from both surfaces to resume normal admission.

A cancellation received after irreversible activation starts returns Too late with current progress. It never rolls back a partial install or revives an obsolete process.

A long-lived command may defer activation indefinitely; report that state rather than claim a deadline. Do not terminate commands or impose a new response limit automatically.

An already-committed security floor remains enforced. A newly announced floor does not disable running work during download, staging, or waiting.

Early floor enforcement or a bounded forced-update policy needs user consultation before implementation; neither is part of the default design.

Do not kill active commands, replay approvals, or silently discard pending requests to install an update. Defer activation when continuity cannot be guaranteed.

Apply quiescence to every component replaced or terminated by the update, including the GUI adapter when bundled with the app. Keep it alive while its requests or permits need it.

On-disk replacement of a bundle containing a running role is activation too. Defer Sparkle installation of that bundle, not merely its process relaunch, until the affected roles quiesce.

Downloading and staging use a separate location and cannot replace resources or code used by the active stack. Prove the updater integration respects this boundary before enabling automatic installs.

An automatic update waits for an idle boundary. A bounded activation attempt that cannot reach quiescence defers the update; it never kills the adapter after that wait.

Record update-caused interruptions and their observed or unknown outcome in the audit log. Crash recovery follows the matrix below; planned relaunch must avoid causing those failures.

#### State across restarts

A clean root-authority update has no pending requests to resume. Never persist sensitive payloads or invent a cross-process handoff to preserve them.

| Restart or replacement | Required request behavior |
| --- | --- |
| App or transport; root authority stays alive | Reconcile retained requests with their original version, age, expiry, and ledger state |
| GUI adapter before permit consumption | Recapture and revalidate; retain the request only when identity and semantics still match |
| GUI adapter with a consumed permit and lost acknowledgment | Mark Unknown; invalidate the old permit and quiesce its actor before any fresh action |
| Unexpected root-authority restart | Cancel unconsumed requests; retain only allowed metadata and explain fresh submission |
| Root-authority restart after consumption | Reconcile verified evidence or report Unknown; never redispatch |

The section 6 restart rule still applies to failures during updates. Command I/O remains owned by the old authority until its attached commands finish.

For UI capture rejected with any authenticated busy-state no-admission result from section 8, retry capture automatically after authority recovery while the same dialog remains present. This creates a new request, never a replayed decision.

After an unexpected root restart, automatically recapture only requests cancelled by that restart whose previous identities are established as never consumed. Preserve original first-seen age and target deadline, and issue a fresh challenge requiring a fresh decision.

Do not recapture explicit denials/cancellations, consumed or unknown actions, expired targets, or changed dialogs as though they were the same pending operation. Quiesce old actors before recapture.

If previous consumption cannot be established, retain Unknown and the existing user-initiated recovery path. If the dialog disappeared, report its observed expiry or unknown disappearance.

Recaptured requests still obey presence routing and current phone eligibility. Bound notification retries without resetting age or automatically dispatching an action.

#### Activate and commit security floors

After replacement, restart affected services through supported mechanisms and recheck peer identity, protocol support, key access, and ledger health. Keep action admission closed during these checks.

Treat the new release's floors as proposed until every affected old role is quiescent and every replacement role passes its checks. Then commit the installed manifest and floors in one durable transaction before reopening admission.

Signed release policy cannot enforce proposed floors merely by being downloaded. Show Security update ready and its blockers while the old permitted stack continues working.

Journal activation progress. After an interrupted activation, keep action admission closed until recovery validates one complete compatible component set against the committed floors.

Before activation, a trusted installation boundary verifies every candidate's signed generation against committed floors and installed generations. Reject out-of-band and feed-based downgrades except explicit authenticated administrative recovery.

The protection must exist independently of the candidate root binary. A self-check in an old executable cannot establish anti-rollback. Prove protected placement or equivalent platform launch enforcement at the feasibility gate.

The root authority also checks its own OS-validated generation before offering authority operations. This is defense in depth, not a substitute for the external activation and placement protection.

Preserve pairing, device keys, audit history, routing preferences, and consumed decisions. A schema migration must never resurrect a consumed request.

#### Failure and recovery

A failed update before floor commit may restore the previous permitted compatible stack when its storage remains readable. After commit, recovery cannot restore a build below the committed floor.

Never automatically roll back below the security floor. Restoring a backup cannot lower authority policy, security generations, or trust. Preserve retained ledger evidence; handle audit/ledger-only loss through section 12's automatic recovery and visible discontinuities, without recreating pending requests or retrying actions.

If rollback is unsafe, show a repair path and keep manual Mac approval available. Do not clear trust or the ledger to make startup succeed.

### Feasibility gate: remote update continuity

Test app and service replacement while the Mac is locked, the screen is off, and a remote command is running. Measure interruption and recovery.

Prove that the interactive user cannot replace root-launched code, race staging, change registrations, or redirect activation through symlinks. Failing any check rejects the update path.

Also prove whether the service-management path needs renewed administrator authorization. Do not promise silent helper replacement before both security and continuity checks pass.

If updates would add recurring physical access, new approval prompts, or forced pairing, consult the user before selecting that design.

## 18. Protocol evolution and mixed versions

Each component declares what it can send, parse, and execute. App release numbers and protocol versions are separate identifiers.

### Explicit compatibility manifest

Start the first implemented wire contract at version 1. This is a proposed contract, not an existing implementation or a claim of compatibility.

| Component | Required declarations |
| --- | --- |
| Android app | Approval wire versions, request schemas it can render, decision purposes, required security features |
| Optional Android ADB bridge client | Bridge stream/framing versions, flow-control features, and ADB-port discovery capability; no approval authority |
| Mac authority | Wire versions, action schemas, signing purposes, minimum security policy, ledger schemas |
| GUI adapter | Local IPC versions, permit schemas, target-app compatibility contracts |
| Command frontend | Local IPC versions and capture/I/O schemas |
| Mac transport | Envelope versions and relay API versions; no action authority |
| Optional ADB bridge endpoint and command | Bridge stream and local-control versions, separate from approval wire versions; no approval authority |
| Relay and push gateway | Transport API and wake-envelope versions; no authority to select an approval format |

Publish exact supported sets in every release manifest and expose them in diagnostics. Do not claim an untested continuous range such as “version 1 and later.”

A future release supporting versions 1 and 2 lists both, with tested features for each. Storage, audit-sync, local IPC, and push formats evolve independently.

### Authenticated negotiation

![Protocol negotiation without silent downgrade](site/dist/figures/negotiation.svg)

*Both endpoints authenticate the advertised sets and selection. Each action still binds to one exact wire version and schema.*

Use a bounded, versioned handshake authenticated by the existing paired identities. Bind both advertisements, fresh nonces, endpoint identities, and the selected version into its transcript.

Use pairing-bound channel identity for the handshake; do not invoke the biometric approval key for routine negotiation. This identity grants no action authority.

Select the highest safe common envelope version for each peer channel. Request schema selection is separate; an envelope may carry a supported older request contract.

#### Select one shared request contract

Before signing a new request, consider exact action contracts supported by the authority and currently authorized phones. Exclude contracts below the security floor or missing required action features.

Choose the contract that supports the largest number of eligible phones; break ties with the highest version. Use authenticated capability records, including last-known records for offline phones.

Every delivery and decision rechecks current negotiated support. A stale record can reduce reachability but cannot weaken verification or grant unsupported authority.

New enrollment, reconnect, or app update does not change an already-signed request. Unsupported phones show the compatibility reason; other eligible phones remain usable.

Authenticate the envelope selection and verify support for the exact request contract before displaying actionable controls.

Do not trust a relay's version claim or retry an older format after a signature failure. Initial pairing binds the same negotiation to its verified enrollment transcript.

#### Verify the envelope and request separately

Bind the request wire version, action schema, feature requirements, and signing purpose to the request and decision. Bind each channel envelope to its authenticated negotiated session.

Each verifier checks both contracts against retained state. Envelope changes cannot alter or translate the signed request.

Use explicit signature domain separation by protocol, version, message type, and purpose. A valid status signature must never verify as an approval.

For multiple phones, retain one immutable request and one consumption ledger. Only phones supporting that request's contract receive actionable controls.

Do not re-encode an existing request into a weaker schema to reach an older phone. Show the incompatibility and required update on that phone.

### Compatible additions and breaking changes

| Change | Rule |
| --- | --- |
| Optional display-only metadata | May be additive if the contract explicitly permits ignoring it |
| Action meaning, target, scope, biometric requirement, or expiry semantics | Requires an understood schema/capability; never treated as optional |
| Canonical encoding, signature input, or incompatible field meaning | Requires a new wire version or explicitly versioned schema |
| Unknown critical field, enum, purpose, or required feature | Reject that operation with a specific compatibility error |
| No safe common version | Keep pairing; disable affected actions and identify which component needs updating |

Unknown optional fields remain covered by the signature. Verify canonical bytes without dropping fields, and reject noncanonical input before deriving authority.

Document defaults in the schema. Missing security fields never imply broader scope, weaker authentication, or a later expiry.

Maintain old safe contracts during staged rollouts where practical. Publish a tested compatibility matrix and migration path before removing a version.

Security floors come from trusted local software or authenticated release policy. Unauthenticated negotiation cannot lower them or impose a fabricated update requirement.

Persist authenticated policy revisions and minimum security generations independently of replaceable binaries. Reject replayed older policy; trusted administrative recovery must explicitly handle any exceptional rollback.

An incompatible device does not block another compatible phone. Read-only history may remain available through a separately supported audit protocol.

Show installed and required versions with an Update action. Do not require pairing again merely because versions differ.

### Acceptance: upgrades and hostile peers

- Test every declared sender/receiver pair, including older phones with newer Macs and the reverse.
- Alter or replay negotiation transcripts; strip features; force older versions; substitute message purposes. Each attempt must fail without an action.
- Test unknown fields, enums, duplicate keys, size bounds, malformed versions, and canonical signatures with shared Swift/Kotlin vectors.
- Test mixed local component builds, old signed helpers, forged update feeds, modified archives, expired signing credentials, and planned signer rotation.
- Interrupt each update and storage migration boundary. Preserve pairing and consumed decisions; never repeat an uncertain action.
- Test update drain with two phones, a pending biometric prompt, an active command, and a consumed GUI permit. Preserve normal lifetimes and apply the restart matrix on failure.
- Test phones supporting v1 and v1+v2: select v1 when it meets the security floor and action features. Require v2 only when the action or floor requires it.
- Attempt same-user replacement of root code, staging, parent directories, symlinks, and service registrations before and during activation. No unapproved code may execute as root.
- Launch an old signed adapter after a floor increase. Reject its privileged RPCs, old certifications, and access to current receiving keys.
- Stage an automatic update during an indefinite command and pending UI approval. Both old work and new requests must continue normally.
- Test signed remote scheduling, cancellation races, revoked-phone controls, replay, artifact substitution, and attempts to lower security floors.
- Attempt app-only replacement during a live adapter permit. Defer both on-disk installation and relaunch without interrupting the action or changing its expiry.
- Stage a higher floor while old work is running. Keep the old role usable until replacement checks pass and the new floor commits.
- Substitute an older signed root authority through both update and out-of-band paths. Reject it independently of its own startup checks.
- Crash before and after the activation/floor transaction. Admit no action with a partial component set or rolled-back ledger/policy.
- Capture UI prompts during installation and unexpected root restart. Recapture only never-consumed requests, preserving age and requiring a fresh decision.
- Submit a command during activation. Retry only an authenticated no-admission response; preserve stdin/PTY behavior, cancellation, and a visible bounded wait.
- Lose the admission acknowledgment and restart either endpoint. Never replay a possibly admitted or consumed command.
- Inject transient witness faults during startup and live trust/control commits. Test immediate restrictive enforcement, delayed permissive effects, automatic intent reconciliation, and accurate pending status.
- Return each of the four busy-state reasons for a command and a short-lived UI prompt. Retry only authenticated submission-bound no-admission results; preserve cancellation, input, original UI age, and target expiry. Test deadline exhaustion, reason changes, indefinite drain, storage exhaustion, and unverified health hints.

### Reference documentation

The platform API choice and updater configuration remain feasibility work. These official references support the design direction:

- [Apple: XPC peer code-signing requirement](https://developer.apple.com/documentation/xpc/xpc_connection_set_peer_code_signing_requirement(_:_:)).
- [Sparkle setup, signed releases, and feed verification](https://sparkle-project.org/documentation/).
- [Sparkle package-update limits](https://sparkle-project.org/documentation/package-updates/).

## 19. Optional remote Android deployment bridge

### Product boundary

Offer an opt-in developer feature for deploying a build from the enrolled Mac to the enrolled Android phone when they are on different networks. This feature carries an ADB connection; it does not turn an ADB operation into a Remozio approval. ADB can install packages, run a shell, and access device data under Android's debugging rules. Explain that scope during setup and show when the bridge is ready or in use.

Do not let a Remozio approval, decision key, push token, or transport credential grant ADB access. Do not reuse the root approval service as an ADB proxy. Keep deployment transport, rate limits, logs, and failure handling separate from approval messages. Give pending approvals precedence over ADB traffic. The root authority supplies an authenticated aggregate pending/clear priority attestation for approval traffic actually eligible for the phone connection. Local-only requests do not assert network contention. The unprivileged transport may relay this attestation but cannot originate its state. Expose no request details or ADB authority through this control. Bind the attestation to the authority incarnation, recipient scope, increasing sequence, and bounded freshness; the bridge validates the authority and scope over authenticated IPC.

The bridge pauses the affected outbound stream while that attestation says pending. Expiry or authority disconnect marks priority state unknown, not clear. Invalidate old-incarnation attestations on root restart and fetch a fresh authoritative aggregate snapshot automatically; the new root computes it from current retained or recaptured requests. Resume when a fresh authenticated clear arrives. Transport restart also triggers snapshot reconciliation, so stale transport state cannot indefinitely renew a pause. Test root and transport restarts, local-only requests, delayed clear, and replayed old-incarnation controls. The Android app prioritizes its approval socket and pauses bridge sends while an approval is pending; this is app-local scheduling, not a phone-to-Mac priority signal. Measure approval latency under a saturated install; shared link contention cannot be eliminated completely. Report an interrupted, timed-out, or self-update-terminated install as **outcome unknown**; never infer install success from a dropped socket or later package state. Disable the feature independently of normal Remozio operation.

The deployment bridge initially supports one active bridge session per Android device. Multiple Mac–Android pair grants may remain enabled. The Android bridge atomically admits one authenticated session and returns Busy to competing Macs, without disabling their grants. The admission reservation covers setup through teardown; an established stream owns it until closed. No competing session may preempt an install. After disconnect or process restart, reconcile and close the old stream before releasing the reservation; each Mac retains its own terminal or unknown ledger outcome. A later session never resolves a previous unknown outcome. This serialization covers Remozio bridge sessions, not independent USB or external ADB use. Test simultaneous admissions from two enabled Macs, teardown races, and reconnect contention. Approval enrollment remains multi-phone as section 5 defines. Other desktop operating systems are a later client implementation against the same bridge protocol. The first installation of the Android companion and ADB pairing still require an authorized local or already-working ADB path. Thereafter, other app updates can use the bridge if Android policy permits `adb install`. One Mac bridge service owns the private, user-scoped operation ledger; commands submit installs to it rather than writing records themselves. Under one exclusive transaction, it refuses a second open install for that phone and stores an open operation ID, target package ID, expected signing identity, version, artifact digest, transition timestamp, and metadata-only start audit event before dispatch. It stores no APK bytes or ADB payloads in the ledger or audit. A completed and valid ADB install response conditionally closes `open` as acknowledged success or acknowledged failure. A lost, interrupted, timed-out, unauthenticated, or malformed response conditionally closes `open` as **outcome unknown**. The terminal transition and metadata-only terminal audit event use the same transaction. A late response, teardown, package observation, or later install cannot change a terminal state; losing concurrent closers write nothing. On service startup, before serving status or admitting an install, the service conditionally closes every orphaned open operation as unknown with its audit event under the same transaction. Crash after recording `open` but before dispatch is conservatively unknown. The operation ledger is the bridge audit source, so no separate event replay can omit a start or terminal transition. Open records cannot expire; after closure, detailed expected-artifact fields have bounded count/time retention, while minimal terminal event metadata follows the section 12 audit-retention policy. Once that event expires, no historical outcome is displayed or reconstructed; expiry never changes an active operation or causes a new install.

Updating the Remozio app that hosts this bridge interrupts its foreground service and connection. Restore the previously connected bridge automatically where Android permits; validate the supported restart path on real devices. At teardown, conditionally close the install as unknown only if it is still open; an earlier acknowledged response remains acknowledged. On the next paired ADB session, and before a later install to the same package even within that session, independently read the currently installed package metadata if available. Show the observed state beside a retained earlier unknown outcome; if its detailed expected-artifact fields expired, say that comparison is unavailable. Do not call a version/signing match proof of artifact equality unless Android exposes a trustworthy digest and that comparison is validated on supported devices; even an exact match proves only the current state, not which command produced it. If package verification is unavailable, say so. A user-requested subsequent deploy starts a new operation ID, never resolves the earlier one, and never runs automatically as a retry of an unknown command. An enabled Mac–Android pair permits subsequent sessions and installs without recurring confirmation. Reconnection does not renew or expand its grant. If the observed state differs from the requested artifact or may reflect an external update, give no automatic downgrade/retry advice. The restarted Android companion may report its own installed version once running, as a current-state observation under the same rule. The user selected best-effort automatic restoration after updates and restarts. The restoration contract below replaces that pending UX decision.

### Connection path

The Android app maintains an outbound authenticated WebSocket over HTTPS to a dedicated unprivileged Mac bridge endpoint. The endpoint runs under a bridge service account distinct from the approval transport account and follows section 3 protected placement and section 16 credential-custody rules for its binaries, configuration, and identity keys. The endpoint is reached through the section 16 internet transport candidate, Cloudflare Tunnel plus its own Access application, or directly on the same LAN after endpoint authentication. Both paths carry the same framed byte stream. The section 16 setup controller provisions and removes its dedicated ingress and Access application through administrator-authorized typed operations. No VPN, inbound phone port, or public ADB listener is required. The phone never receives Cloudflare management credentials.

The Mac command starts a session on demand and binds a temporary listener on `127.0.0.1` for the local ADB client. Run that command and its `adb` client as the same configured administrator account that owns the existing paired ADB host key; keep the key unreadable to other local accounts and never copy it between accounts. If the developer uses a different account, admission and setup UX need a separate decision; no per-deploy elevation is silently introduced. Authenticate the command to the background bridge with a local peer check. Loopback binding alone does not authorize the ADB client: other local processes can race to the port. The command opens the listener only for its own immediate `adb connect`, admits exactly one connection, and closes it on failure or timeout. This narrows the race but cannot exclude a malicious same-user process with the ADB host key. Retain the local peer checks and test the disclosed same-user exposure; do not add a per-session phone approval. Bind each connection to one enabled Mac–Android pair and fresh authenticated session state. Connection challenges may expire, but the pair grant has no arbitrary expiry. Close both sides on disconnect, timeout, revocation, or explicit Stop. Do not accept arbitrary hostnames, ports, or destinations from either endpoint.

In wireless-debugging TLS mode, the phone app discovers the current `_adb-tls-connect._tcp` service and its port, validates that it is the device's own wireless-debugging service, and connects to that port on loopback. A manual port entry is a fallback when discovery fails. It must not scan the LAN or relay traffic to other devices. Android 17's local-network permission and NSD picker behavior need a real-device feasibility test; use the narrowest supported permission path. The observed Pixel 11 Pro accepted a loopback connection to its current wireless-debugging port on 30 September 2026. That proves local reachability for this device, not discovery or permission behavior for a shipped app.

Offer two ADB transport modes: native wireless-debugging TLS and opt-in legacy TCP ADB for mobile-data fallback. In native TLS mode, forward ADB bytes without terminating or replacing ADB’s own TLS. Retain Android’s host authorization in both modes; Remozio enrollment and Access are additional gates. Legacy mode permits plaintext ADB only on the on-device hops described below. Remozio does not expose an unauthenticated public ADB port. Use bounded binary frames, backpressure, idle limits, and bounded buffers so neither endpoint nor intermediary buffers unbounded session data. Do not impose an arbitrary total transfer cap or require a large-install approval for an enabled pair. Keep APK bytes and ADB payloads out of application logs and audit history.

### Mobile-data fallback with legacy ADB

Support a phone with no available or working Wi-Fi connection. An explicitly enabled legacy listener, such as one started with `adb tcpip 5555`, can supply the local ADB endpoint while the bridge uses mobile data.

| Segment | Protection |
| --- | --- |
| Mac ADB client to Mac bridge | On-device loopback; plaintext ADB is acceptable |
| Mac bridge to Android bridge, including any relay | Authenticated end-to-end encryption with pinned enrolled peers |
| Android bridge to its own adbd | On-device loopback; plaintext ADB is acceptable |

The outer encrypted session is mandatory in both modes. HTTPS to Cloudflare alone does not provide this end-to-end property because the edge terminates that connection. Encrypt bridge frames between Mac and Android using a reviewed authenticated channel, with separate bridge keys and purpose binding. Bind peer identities, selected ADB mode, session identity, and protocol negotiation; reject replay and unauthenticated mode changes before forwarding bytes.

Keep ADB host keys on the Mac. Preserve Android’s normal host-key authorization; do not disable authentication or auto-accept an unknown host. Legacy host authorization and wireless-debugging pairing are distinct setup paths. Do not assume one automatically authorizes the other.

Setup explains that legacy ADB lacks native transport encryption, that Remozio encrypts the inter-device path, and that local processes remain within the endpoint threat model. Offer an explicit legacy-fallback preference. Once enabled and provisioned, loss of Wi-Fi may select it without a recurring confirmation; show the active mode and connection reason.

Legacy setup needs an already-authorized ADB path, commonly USB or a working wireless connection. Do not promise that an ordinary Android app can enable TCP ADB from scratch after Wi-Fi has already become unavailable. Verify listener lifetime across Wi-Fi loss, app updates, device reboot, and vendor implementations; show required recovery actions when it stops.

Connecting to loopback does not prove that adbd listens only on loopback. Check the actual listener exposure on supported devices, including IPv4, IPv6, Wi-Fi, tethering, and mobile data. Prefer supported loopback binding or effective isolation where available. Never describe a wildcard listener as device-local or rely on carrier NAT as isolation.

If the system listener remains reachable beyond the device, disclose that separate risk clearly during legacy setup. Explain how to disable it and that stopping the Remozio tunnel alone may leave it running. A verified loopback-only configuration is preferable, but is not an artificial prerequisite for offering the explicitly caveated legacy option. Do not silently alter system networking or add a VPN requirement.

The bridge still targets only the phone’s own configured ADB port and exposes only a loopback listener on the Mac. No arbitrary network destinations, LAN scanning, public raw ADB forwarding, or plaintext inter-device fallback are allowed.

On mode changes or network loss, reconcile the existing operation as specified by the ledger before opening a fresh authenticated connection. Reconnection or switching modes must never resend an install or command whose outcome is unknown.

Test a complete install with Wi-Fi disabled and mobile data active. Test native-TLS-to-legacy transition, unknown ADB hosts, listener exposure, relay interception, rejected mode changes, Stop, revocation, reboot, and interrupted-install recovery. Measure throughput and mobile-data use. Both modes retain the same admission, audit, and no-replay contract.

### Restore a previously connected bridge

If the bridge was connected before an app update or restart, preserve its desired connection state and try to restore Ready mode and reconnect automatically. Apply this to ordinary process restarts and to device restarts where the platform permits recovery.

Retain the existing pairing and grants. Authenticate both peers again, check protocol compatibility and revocation, rediscover the current ADB endpoint, and obtain a current valid lease. Never revive an expired lease or extend authorization merely because recovery started.

Restoration targets the previously connected, authorized peer. It does not enroll another Mac or grant access to another Android device. Keep existing concurrency and session-admission limits.

Record explicit Stop, disabled Ready mode, and revocation durably. They cancel restoration and pending reconnect attempts. Recheck them before admitting a restored connection. Being paired or formerly Ready without a connected bridge is not sufficient to start recovery under this policy.

Use bounded retries with backoff while recovery remains desired. Show Reconnecting until the connection is verified, then Connected. Report Unavailable with the actual reason when Android or ADB prevents recovery; do not claim success from saved state alone.

Avoid a recurring phone action when the retained grant and platform permit automatic restoration. If user interaction is required, identify the minimum action and preserve pairing. Do not bypass force-stop, permissions, wireless-debugging controls, or foreground-service restrictions.

Restoring transport never repeats an install, shell command, approval, or other operation. Retain terminal and unknown outcomes under the existing operation ledger rules.

Pair enablement is the persistent ADB authorization. Recover transport automatically for an enabled pair without recurring consent. Stop disables the selected pair and cancels reconnect attempts; an idle transport closure does not disable it.

Test self-update, normal restart, process death, and device reboot on supported Android versions. Include locked-device recovery, explicit Stop during retry, revoked or expired grants, changed ADB ports, offline peers, and unsupported protocol versions. Verify that recovery never duplicates an operation.

### Ready mode and platform availability

The user explicitly enables a **Ready for remote deployment** mode in the Android app. While ready, a foreground service owns the outbound connection and shows a persistent notification with Stop. Use the Android `connectedDevice` foreground-service type only if its platform and distribution requirements are met; otherwise Ready mode remains unavailable until a compliant service design is validated. The app visibly reports an active ADB session and lets the user end it immediately. Ready authorization is off by default and scoped to a Mac–Android pair. Once explicitly enabled, that pair permits unattended sessions and installs until disabled or revoked, with the disclosed same-user exposure. No recurring biometric, per-install confirmation, or arbitrary grant expiry is imposed. Idle connections may be released to save resources without clearing the grant. Validate an on-demand wake path before relying on idle teardown; a grant alone cannot bypass Android background restrictions.

If Android stops the service, the phone is force-stopped, neither the selected local ADB service nor an enabled and provisioned fallback can be reached, or the phone is offline, report **Unavailable** on the Mac. An ordinary app cannot read Android's wireless-debugging trusted-network state through a public API, silently enable wireless debugging, or promise unattended access after every reboot or network change. Android 17 ADB Wi-Fi 2.0 can re-enable debugging on a previously trusted network; test the exact behavior on supported devices. Push may notify the user to reopen Ready mode, but does not replace the live socket or bypass Android's background-start limits.

Pairing, Access service credentials, and the phone bridge identity can each be revoked. Revoking the Remozio phone or disabling this feature closes active ADB sessions and prevents new ones. The Android app stores bridge credentials in installation-protected storage and never places a shared secret in the APK. The Mac keeps its ADB pairing key in ADB's existing host store; do not copy it to the relay. The bridge's local status identifies the paired Mac and last session, but does not expose ADB command contents.

### Feasibility and acceptance

Before implementation, prove on the supported Android 17 device that an ordinary installed app can discover the local ADB TLS port, connect to it under the intended target SDK and local-network permission model, and maintain a foreground WebSocket in idle, Doze, lock, and network transitions. Verify Cloudflare Access service authentication on a WebSocket upgrade through the configured Tunnel, including edge idle timeouts, keepalive behavior, credential rotation, and revocation. Prove the selected foreground-service type meets platform and distribution rules. The current Tailscale-based experiment may remain a development-only comparison during measurement; VPN use is not a product requirement.

Acceptance: a paired Mac builds an APK, starts the bridge on demand, runs `adb connect 127.0.0.1:<local-port>`, and installs the APK with app data preserved; a different Mac or revoked phone is rejected; the phone can stop an active transfer; an unavailable selected ADB listener is reported unless an explicitly enabled, provisioned fallback can establish a fresh authorized connection; restart and changing ADB ports recover without re-pairing when Android permits; a saturated install receives lower priority than a pending approval; Access or relay failure never exposes ADB publicly. Test a competing local process racing the ADB listener, a same-user process holding the host key, and the selected admission policy. Test Mac-local lease renewal, expiry without clear, recovered clear, Android app-local scheduling, and bridge interruption without approval payload leakage. Self-deploy the bridge-hosting app and interrupt an ordinary app install; verify session teardown, durable unknown command outcome, and separate current-state observation before any later same-package install, including within one session. Cover concurrent install commands, crash before dispatch, crash after dispatch, crash after response but before terminal commit, teardown racing a response, timeout racing a late response, malformed response, observed expected state, unchanged prior state, a third externally changed state, byte-identical reinstall, unavailable verification, expired detailed evidence, and the chosen Ready-mode recovery. No losing event, later observation, or later install may reclassify a terminal outcome. Verify atomic metadata-only start/terminal audit and revocation during an active stream. Measure time to readiness, connection time, install throughput, battery use, and long-running socket survival on real hardware.

Platform references: [ADB daemon listener configuration](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/main/daemon/main.cpp), [ADB wireless debugging](https://developer.android.com/tools/adb), [ADB Wi-Fi service discovery](https://android.googlesource.com/platform/packages/modules/adb/+/refs/tags/android-vts-15.0_r2/docs/dev/adb_wifi.md), [Android 17 local-network permission](https://developer.android.com/privacy-and-security/local-network-permission), [foreground-service types](https://developer.android.com/develop/background-work/services/fgs/service-types), [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/), and [Access service tokens](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/).
