Skip to content

refactor(settings): declare settings once and admit them per setting - #3662

Open
thymikee wants to merge 4 commits into
mainfrom
refactor/declarative-settings-admission
Open

thymikee wants to merge 4 commits into
mainfrom
refactor/declarative-settings-admission

Conversation

@thymikee

@thymikee thymikee commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Summary

settings now declares every setting once and lets admission check support per setting.

  • One table. contracts/src/settings-catalog.ts holds each setting's forms, app source, app scope and help. CLI/daemon parsing (one parser), usage, the invalid-args message, settingsAppScope, the client type, the MCP setting enum and settings --help derive from it.
  • Per-setting admission. Owners declare settingsRuntimeOperationFacts({ target, unsupported, write, read }); admission checks the requested setting before binding, so a refusal never expires the ref frame. Each owner's fact record is keyed by the setting list its dispatch switch is exhaustive over, so the two cannot drift. This removes the daemon's pre-admission refusals and the Apple leaf guards (ADR 0019 note added).
  • pollUntilSettled in host-kit/retry replaces the Android alert and fill-verification loops.

Public changes (CHANGELOG): an unserved setting answers UNSUPPORTED_OPERATION with the owner's reason, replacing macOS INVALID_ARGS and setting-unsupported-on-leaf; the parser refuses a permission action or mode its target does not take and stray extra arguments; Android alert wait 0 reports a visible alert.

agent-device settings wifi on --platform macos   # UNSUPPORTED_OPERATION, reason unsupported-platform-leaf

51 files, +1480/−1779: over the 2,500-line gross guide; one migration kept as one PR on request, net −299.

Validation

  • pnpm check:affected --run passed at 38bfc3a60.
  • Live run at 38bfc3a60 on an Android 15 emulator, an iPhone 17 Pro simulator (iOS 27.0) and the macOS host; transcript in the PR comments.

🤖 Generated with Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 49 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/commands/capture/settings.test.ts
Comment thread packages/daemon/src/__tests__/support/snapshot-runtime-fixture.ts Outdated
Comment thread packages/host-kit/src/internal/retry.ts
Comment thread packages/platform-android/src/runtime.test.ts Outdated
Comment thread packages/contracts/src/settings.ts Outdated
Comment thread packages/host-kit/src/internal/retry.test.ts Outdated
Comment thread src/commands/capture/settings.ts
Comment thread packages/platform-apple/src/system/runtime.test.ts
Comment thread packages/platform-apple/src/system/runtime.test.ts
Comment thread packages/platform-android/src/__tests__/fill-verification.test.ts
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3662/

Built to branch gh-pages at 2026-10-15 13:34 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@thymikee

Copy link
Copy Markdown
Member Author

The settings catalog and per-setting admission look sound in code at e0a7c21, but this PR changes device-facing routes and has no device run yet. CI is pending: all 20 non-passing checks are still queued, none have failed, and the diff overlaps command-docs-gate, Repo Guards/layering, Bundle Size and the Android/iOS smoke routes. I know of no conflicts. Before merge, please post a live run at head.

The Android alert wait/accept/dismiss and fill-verification loops now run on pollUntilSettled (alert.ts:154), and alert wait 0 changes behavior. Every settings request on every target now goes through the new strict daemon parser and per-subject admission. The Apple owner guards that refused text-size and biometrics on tvOS and visionOS are gone, so admission is now the only refusal. Admission is unit-tested only against a condensed copy of the owner facts (snapshot-runtime-fixture.ts:173), never against the shipped owner tables through admitRuntimeOperations. A mismatch between a real owner table and the daemon admission, or a timing change in the sampler, would reach users without a device run. Please post a run on an Android emulator showing: alert wait 0 --json with a dialog already visible returns the alert; alert accept dismisses it; fill @eN hello reports verified text; settings animations off succeeds; settings faceid match returns UNSUPPORTED_OPERATION with reason unsupported-platform-leaf. On an iOS simulator: settings appearance dark and settings text-size large, then bare settings text-size reads large; after snapshot, settings animations off returns UNSUPPORTED_OPERATION and click @eN on the same ref still works, which shows the frame did not expire. On macOS: settings wifi on --platform macos --json returns UNSUPPORTED_OPERATION with reason unsupported-platform-leaf. Moving the pollUntilSettled commit to its own PR and running only the settings part here would also clear this.

Not blocking, and fine to take or leave: commit f11c7a3 (pollUntilSettled, the Android alert and fill rewrites, and their tests) is unrelated to declaring settings, carries its own public change in alert wait 0, and drives much of the churn, so it could be its own PR with its own Android run; ANDROID_ADB_SETTINGS (settings-runtime.ts:145) and every other owner's served list restate that owner's dispatch switch with nothing tying them together, so a rule that each owner derives its served settings from the same handler table keyed by SettingName (with the Android list moving to platform-android) would remove the class; the daemon reads app === 'positional' as "this setting requires an app" (snapshot-settings.ts:276), which turns a wire-encoding detail into a requirement rule, so declaring appRequired in the catalog would be clearer; and parseSettingsMutation trims the state (settings.ts:267) so the daemon now accepts on while the comment at settings.ts:421 still says padding is rejected, so either stop trimming or drop that claim and add a CHANGELOG line.

I ran no tests, and I did not regenerate settings --help or the MCP schema, so I have not compared the derived usage with the website docs. I could not confirm that the new frame-survives-refusal handler tests fail on main. I also did not measure bundle or eager-closure growth from the new contracts subpath.

The cubic-dev-ai threads that still apply are the Limrun owner-list restatement (#3662 (comment)) and the settings-catalog help dropping per-owner --app resolution (#3662 (comment)), plus 11 lower-priority ones. These threads do not apply and can be resolved:

@github-actions

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.25 MB 5.25 MB +4.0 kB
Package (unpacked) 5.25 MB 5.25 MB +4.0 kB
Package (download) 1.58 MB 1.58 MB +1.0 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 22.1 ms 22.2 ms +0.1 ms
CLI --help 61.4 ms 63.6 ms +2.1 ms

thymikee and others added 4 commits October 15, 2026 18:15
Add pollUntilSettled to host-kit/retry: it samples a probe until a
settled check accepts or a deadline passes, always takes the first
sample, lands the last one at the deadline, and stops on an aborted
signal. The Android alert wait, alert-dismiss confirmation, and fill
verification use it instead of their own loops, and their tests drop
the cadence assertions the helper's tests now own.

An already-visible Android alert now answers alert wait 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One catalog row per setting (settings-catalog.ts) now carries its forms,
app source, app scope, and help. CLI and daemon parsing, usage, the
invalid-args message, app scope, the MCP setting enum, the public client
type, and settings --help are derived from it.

Owners declare settings support per setting through
settingsRuntimeOperationFacts({ target, unsupported, write, read }), and
admission checks the requested setting's fact before binding. That
replaces the daemon's pre-admission macOS and Apple text-size refusals
and the Apple owner leaf guards: an unserved setting answers
UNSUPPORTED_OPERATION "settings <setting> is not supported on this
device" with the owner's reason and hint, and the ref frame survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee
thymikee force-pushed the refactor/declarative-settings-admission branch from e0a7c21 to 38bfc3a Compare October 15, 2026 16:21
@thymikee

Copy link
Copy Markdown
Member Author

Rebased on main and pushed 38bfc3a60; pnpm check:affected --run passes there. Live run at that head (fresh pnpm build, build:android, clean:daemon; Android snapshots report android-helper 0.21.29-dev).

Android 15 emulator (ReactNative_API_35)

  • alert wait 0 --json with the system uninstall dialog already up: success, waitedMs: 283, alert Do you want to uninstall this app?. alert dismiss → Alert dismissed. Reopened, alert accept → handled: true, button: OK, and alert get then reports No alert visible.
  • fill @e8 hello → Filled 5 chars with no mismatch warning; the next snapshot shows the field holding hello.
  • settings animations off → success; window_animation_scale and animator_duration_scale read 0, then on restores 1. settings text-size → large; settings wifi TRUE → success.
  • settings faceid match and settings reset-keychain clear → UNSUPPORTED_OPERATION, unsupported-platform-leaf.
  • settings permission deny accessibility → INVALID_ARGS settings permission deny does not apply to accessibility. Use grant|reset.; settings clear-app-state <app> typo → INVALID_ARGS usage.

iPhone 17 Pro simulator, iOS 27.0

  • settings appearance dark, settings text-size extra-large, then bare settings text-size → extra-large; simctl ui agrees (dark extra-large). Restored to light/large, and the read returns large.
  • After snapshot -i: settings animations off and settings fingerprint match → UNSUPPORTED_OPERATION, unsupported-platform-leaf; click @e6 on the same ref → Tapped @e6, which opens General.

macOS host

  • settings wifi on, settings text-size and settings clear-app-state <app> → UNSUPPORTED_OPERATION, unsupported-platform-leaf, with the host hint.
  • settings permission grant accessibility full → INVALID_ARGS takes no mode; --app on it → setting_app_not_consumed.

Response to the non-blocking points:

  • Served lists. Each owner's dispatch now owns its list and switches over it exhaustively (satisfies never). The owner's fact record is typed Record<ThatList, RuntimeOperationFact> through a type-only import, so a missing or extra name on either side fails to compile. ANDROID_ADB_SETTINGS and servedSettings are gone from contracts; Android's list lives in platform-android/src/settings.ts, and Limrun's two legs key off the Android and Limrun iOS lists.
  • appRequired. Declared in the catalog for clear-app-state; the daemon reads it instead of app === 'positional'.
  • State padding. Kept the trim and made the on/off read trim too, so every vocabulary accepts the same padding. The stale comment is gone, and the CHANGELOG says so.
  • Split. Kept as one PR as requested; the Android run above covers the pollUntilSettled loops.
  • Admission coverage. Each owner's runtime test now reads its shipped facts through runtimeSubjectFact, the same lookup admission runs. That covers the Android per-setting facts, the Apple iPadOS row and the refusal reasons. The daemon fixture branches per platform.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant