Skip to content

feat(settings): let permission and iOS location settings target an explicit app - #3205

Merged
thymikee merged 3 commits into
mainfrom
t3code/finish-issue-3179
Oct 5, 2026
Merged

thymikee merged 3 commits into
mainfrom
t3code/finish-issue-3179

Conversation

@thymikee

@thymikee thymikee commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Summary

settings permission and the iOS-simulator settings location on|off privacy change acted only on the app the session had open, so granting a permission before an app's first launch meant opening the app first just to bind it. simctl privacy and Android's pm need only the id, so the only real requirement was that the daemon was never handed one.

agent-device settings permission grant camera --app com.example.app
agent-device settings location on --app com.example.app
await client.settings.update({ setting: 'permission', permission: 'camera', state: 'grant', app: 'com.example.app' });

The named app rides the request's input payload the way a gesture payload does: app is no CLI flag key, and permission's positionals already carry target and mode. The CLI reaches it through the --app option doctor already owns; client and MCP through their app field, now described (it was on the undescribed-inputs baseline). clear-app-state still carries its app positionally.

A mutation that cannot consume an app refuses rather than silently dropping it: Android's location on|off writes the device's location_mode, and a macOS permission is a host TCC grant. settingsAppScope is that one table, keyed on a target family the daemon derives with the kernel's own predicates so the contracts façade keeps its eager-closure budget; the refusal carries setting_app_not_consumed with dispatched: no. No app has to be running; the Android revoke-kills-the-app rule (#1796) is unchanged. 11 files.

Validation

Tested at 5834b3e51: pnpm check:affected --run all runnable checks passed (format, lint, typecheck, layering, fallow, build, 531 related test files / 4129 tests, command-docs). New coverage in packages/contracts/src/settings.test.ts (scope table + refusal), src/commands/capture/settings.test.ts (input carrier, clear-app-state and location set excluded), and snapshot-settings-handler.test.ts (iOS permission/location with no session app, explicit app beating the session app, Android permission, and both refusals asserting the owner was never reached). scripts/__tests__/eager-closure-budgets.test.ts pins that the contracts façade gains no evaluated module.

Residual risk: sharing --app means AGENT_DEVICE_TARGET_APP and user-config targetApp now also default the app for settings, matching doctor's existing precedence; project config is unaffected (projectConfig: false). An .ad script cannot yet round-trip the app — it renders positionals, and a nameless replay hits the owner's existing session_app_required refusal (#3194) rather than granting to the wrong app. Device lanes (iOS/Android replay) are GitHub-authoritative and cover this route on the head.

Closes #3179

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.04 MB 5.04 MB +4.1 kB
Package (unpacked) 5.04 MB 5.04 MB +4.1 kB
Package (download) 1.51 MB 1.51 MB +1.4 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 29.1 ms 27.8 ms -1.4 ms
CLI --help 85.8 ms 84.5 ms -1.3 ms

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-05 09:01 UTC

@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 11 files

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

Re-trigger cubic

Comment thread packages/contracts/src/settings.ts Outdated
Comment thread src/commands/capture/settings.test.ts Outdated
Comment thread website/docs/docs/commands.md Outdated
Comment thread src/commands/capture/settings.ts Outdated
Comment thread src/daemon/handlers/snapshot-settings.ts Outdated
Comment thread src/commands/capture/settings.ts Outdated
@thymikee
thymikee force-pushed the t3code/finish-issue-3179 branch from acaf809 to 5834b3e Compare October 4, 2026 08:17
@thymikee

thymikee commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

I reviewed 5834b3e and found two defects that change which app a settings command targets, plus missing device evidence. The 21 checks pass, and there are no conflicts.

First, locationAppScope in packages/contracts/src/settings.ts accepts only the literals 'on' and 'off'. parseSettingState also accepts true/1/false/0, and both the Apple and Android owners accept them. So settings location 1 --app X is classified 'unknown', and settingsWriteAppId in snapshot-settings.ts drops the explicit app. On an iOS simulator with session app Y, this grants or revokes location for Y and still reports success. On Android, the setting_app_not_consumed refusal is skipped and --app is silently ignored. The rule: the app-scope table must classify every state the owners' parser accepts. Please build it from the SETTING_STATE_ON/OFF vocabulary (or a non-throwing variant of parseSettingState), and add handler tests for location true and location 0 with an explicit app on iOS and on Android.

Second, allowedFlags: ['targetApp'] in src/commands/capture/settings.ts lets env defaults and user config (cli-config.ts lines 169-193) fill flags.targetApp for settings. The reader copies it into input.app, and the daemon ranks that above session.appBundleId (snapshot-settings.ts:403). Someone who set AGENT_DEVICE_TARGET_APP or a config targetApp for doctor now has settings permission grant camera mutate that app instead of the session app. Android settings location on and macOS settings permission also start failing with setting_app_not_consumed. The rule: a mutation's app must come only from an explicit per-invocation argument. Please read --app for settings only when it was given on the command line, or give settings its own non-configurable flag key. If you want to keep the shared key, document the precedence in commands.md and the CHANGELOG, and make env-sourced values yield to the session app.

Third, this is a device-facing change, but I see no live run. simctl privacy grant <svc> <explicit bundle> and Android pm grant <explicit package> now run without a session app, and only fixture owners and helper tests cover them. Issue #3179's "Done when" case, a permission set on an app the session never opened, has no evidence on a real target. Please run on an iOS simulator and an Android emulator, each with a session that never opened the app. Run settings permission grant camera --app <installed id> and show the success response with the explicit app id. Then show the grant landed: simctl privacy state or no permission prompt on iOS, and adb shell dumpsys package <pkg> | grep CAMERA showing granted=true on Android. Also show settings location on --app <id> succeeding on iOS and refused with setting_app_not_consumed on Android. After the first fix, add one iOS settings location 1 --app X run with a different session app, showing that X receives the grant.

Fourth, settings.test.ts:193 never passes targetApp in the clear-app-state test, so the exclusion branch at settings.ts:70 can be deleted and the test stays green. Please pass targetApp: 'com.other.app' and assert positionals ['clear-app-state','com.example.app'] with input undefined.

Could a smaller design cover this? Send input.app for every setting except clear-app-state. In the daemon, admit the explicit app only for non-macOS permission and for Apple on/off location, using parseSettingState to decide on/off, and refuse it everywhere else. That one rule would replace the 'unknown' verdict and the writer-side settingsInputApp filter, and it would remove the silent drops. Before that, please decide whether --app for settings may come from env or config defaults.

Not blocking: the field description at settings.ts:46 says the app "must not be running" (it should say "need not be running"), commands.md says "permission grant" where it should say "permission change" since deny and reset are also targeted, and location set --app and wifi --app are silently dropped by settingsInputApp, so either refuse them in the daemon or document that they are ignored. Take or leave these.

The open inline threads still apply: the on/off location states, the clear-app-state test, the commands.md wording and the "must not be running" text. All four still hold, and none can be resolved yet.

I did a read-only review and ran no tests. I did not trace the MCP schema surface and assumed it uses the same settingsDaemonWriter. Before merge, locationAppScope must cover every accepted state, env and config defaults must stop retargeting settings mutations, and the live iOS and Android explicit-app runs must be posted.

`settings permission` and the iOS-simulator `settings location on|off` privacy change acted only on
the app the session had open, so setting a permission before an app's first launch meant opening the
app first just to bind it. `simctl privacy` and Android's `pm` need only the bundle id or package, so
the requirement was the daemon never being handed one.

The named app now rides the request's input payload the way a gesture payload does: `app` is no CLI
flag key, and permission's positionals already carry the target and mode. The CLI reaches it through
the `--app` option `doctor` already owns, and the client and MCP surfaces through their `app` field,
which is now documented. `clear-app-state` keeps carrying its app positionally, unchanged.

Where a mutation cannot consume an app at all the request is refused rather than silently dropped:
Android's `location on|off` writes the device's `location_mode`, and a macOS permission is a TCC grant
to the host process. `settingsAppScope` is that one table, and the refusal carries
`setting_app_not_consumed` with `dispatched: no` so a driver branches on the reason and knows no
device was touched.

No permission is set on a device that never held the app installed differently: the owner resolves the
id as it always has, and the Android rule that revoking a granted permission kills the app (#1796) is
unchanged.
@thymikee
thymikee force-pushed the t3code/finish-issue-3179 branch from 5834b3e to 58f310b Compare October 4, 2026 19:38
@thymikee

thymikee commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

Both code defects from the earlier review (5834b3e) are fixed at 58f310b, but the live evidence for the explicit-app path is still missing. Thanks for the quick fixes.

The PR body says the device lanes cover this route. They do not: no e2e or replay calls settings with --app. settingsWriteAppId now passes an explicit app with no session app to simctl privacy grant <svc> <bundle> on iOS and pm grant <pkg> on Android. Only the fixture owners and handler tests reach these paths. So issue #3179's done-when case, granting a permission to an app the session never opened, has no proof on a real target. We do not know that simctl and pm accept the id without a launched app, or that the grant lands. Please post one live run per platform with a session that never opened the target app. On an iOS simulator, settings permission grant camera --app <installed id> should succeed naming that id, and the simctl privacy state should show the grant. Also run settings location 1 --app X with a different session app Y, and show that X receives the grant, not Y. On an Android emulator, settings permission grant camera --app <pkg> should succeed and adb shell dumpsys package <pkg> | grep CAMERA should show granted=true. Then settings location on --app <pkg> should be refused with details.reason setting_app_not_consumed and dispatched: no. The refusal happens in settingsAppRefusal, which also refuses macOS permission before admission.

Of the earlier inline threads, the location-state parsing one, the clear-app-state test, the commands.md wording, the clear-app-state input path and the running-app help text are fixed at this commit, so please resolve them. The location set app thread still applies: settingsInputApp drops the app, so settings location set <lat> <lon> --app X ignores X without any message. That is a dropped argument, not a wrong-app change, so it is your call whether to fix it here.

The code looks good at 58f310b. The Smoke Tests failure looks unrelated: smoke:automation-input stalled at wait text Agent Device Tester with wait_readiness_exhausted in the runner-start phase, and the settings handler shares no code with that route. The 01-settings.ad timeout was in the Apple Settings app UI and passed on retry. I did not run the tests myself. The remaining step before merge is the two live runs above.

`settingsInputApp` dropped `--app` for `location set`, so
`settings location set <lat> <lon> --app X` ignored X without a word — a
dropped argument, not a wrong-app change, and a contradiction this PR's own
rule forbids ("a device-wide change refuses a named app instead of dropping
it"). The daemon's scope table already classifies `location set` as
device-level for every family; the writer just never forwarded the app for
that refusal to land. Forward every `location` state's app so the daemon's
`setting_app_not_consumed` refusal answers it before dispatch, and widen the
client `location set` leg to carry `app` so the writer can.

Co-Authored-By: opencode <noreply@opencode.ai>
@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

The code side of this PR is in good shape at 05b1b4c, but the live runs I asked for at 58f310b are still missing, so it is not ready yet. The earlier code findings are fixed, and I have no new code findings.

The runs matter because the explicit-app path in settingsWriteAppId only runs in fixture and handler tests. No e2e or replay passes --app. That leaves issue #3179's main case unproven on a real target: granting a permission to an app the session never opened. It also leaves unproven that an explicit app X beats a session app Y for iOS location. Please post one live run per platform, from a session that never opened the target. On an iOS simulator, settings permission grant camera --app <installed id> should succeed naming that id, and the simctl privacy state (or TCC.db) should show the grant. Then, with session app Y open, settings location on --app X should land on X and not on Y. On an Android emulator, settings permission grant camera --app <pkg> should succeed, and adb shell dumpsys package <pkg> | grep CAMERA should show granted=true. Then settings location on --app <pkg> should be refused with details.reason setting_app_not_consumed and dispatched no.

The six cubic-dev-ai threads (location state parsing, clear-app-state test, commands.md wording, location app forwarding, clear-app-state input, field description) are all fixed at this head, so please resolve them. Cubic has not reviewed 05b1b4c yet.

The Android smoke check failed at step 30, wait text landscape after a rotate. That route is a rotate followed by a wait poll and shares no code with the settings writer, scope table or handler, so it looks unrelated. I read only the tail of that log, so I matched the failure to the route and did not find its root cause. The only settings call in that lane is the full-tier cleanup with no --app, and this run was the smoke tier. I did not run tests or device commands myself. There are no conflicts. Once the two live runs are posted, this should be ready for a final human review.

@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

Live device evidence for the explicit-app path, run on head 05b1b4c (includes the location set refusal). Each run used a session that never opened the target app. Built from this branch, CLI used directly, isolated daemon state dir.

iOS simulator (iPhone 17 Pro, iOS 26.2)

Camera grant to an app the session never opened (Maps, com.apple.Maps):

$ agent-device settings permission grant camera --app com.apple.Maps --session s-x --json
{ "success": true, "data": { "setting": "permission", "state": "grant", "message": "Updated setting: permission" } }

$ sqlite3 <sim>/data/Library/TCC/TCC.db "select service,client,auth_value from access where service like '%Camera%'"
(before)  <no rows>
(after)   kTCCServiceCamera|com.apple.Maps|2

Location with a different session app. Session s-y opened Safari, then:

$ agent-device open com.apple.mobilesafari --session s-y
Opened: com.apple.mobilesafari
$ agent-device settings location on --app com.apple.Maps --session s-y --json
{ "success": true, "data": { "setting": "location", "state": "on", "message": "Updated setting: location" } }

Location authorization on the simulator lives in locationd's clients.plist, not TCC.db:

icom.apple.Maps:   Authorization = 2
(no entry for com.apple.mobilesafari)

Maps got the grant; Safari, the session app, did not.

Note: the iOS success message reads Updated setting: permission without repeating the bundle id; the id is only visible via the resulting simulator state above. The Android permission response lists the granted permission names. Neither is a functional problem.

Android emulator (Pixel 9 Pro XL, API 37)

Camera grant to an app the session never opened (com.google.android.gm):

$ adb shell dumpsys package com.google.android.gm | grep -i "android.permission.CAMERA: "
(before)  android.permission.CAMERA: granted=false
$ agent-device settings permission grant camera --app com.google.android.gm --session a-x --json
{ "success": true, "data": { "setting": "permission", "state": "grant", "permission": "camera",
  "permissions": ["android.permission.CAMERA"], "message": "Updated setting: permission" } }
(after)   android.permission.CAMERA: granted=true

Device-wide location with an app named is refused before anything is dispatched:

$ agent-device settings location on --app com.google.android.gm --session a-x --json
{ "success": false, "error": { "code": "INVALID_ARGS",
  "message": "settings location on applies to the target itself, not to an app: com.google.android.gm names nothing it can grant or revoke.",
  "hint": "Drop the --app option and run `settings location on`, or aim the app at an app-scoped setting such as `settings permission grant location --app com.google.android.gm`.",
  "details": { "reason": "setting_app_not_consumed", "dispatched": "no", "setting": "location on", "app": "com.google.android.gm" } } }

Result

Case Result
iOS permission grant, never-opened app pass
iOS location on --app X with session app Y: X granted, Y not pass
Android permission grant, never-opened app (granted=true) pass
Android location on --app refused, setting_app_not_consumed, dispatched no pass

No code changes were needed. I did not exercise the macOS refusal or the earlier handler paths beyond what is above.

@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

Thanks for the live runs at 05b1b4c. They cover every case I asked for: the iOS camera grant lands in TCC.db for an app the session never opened, settings location on --app X grants Maps and not the session app Safari, the Android camera grant flips to granted=true, and Android location on --app is refused with setting_app_not_consumed and dispatched no. With the code findings already fixed, all review threads resolved, and no conflicts, this is ready for a final human review.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 5, 2026
@thymikee
thymikee merged commit ab25531 into main Oct 5, 2026
21 of 22 checks passed
@thymikee
thymikee deleted the t3code/finish-issue-3179 branch October 5, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(settings): let permission and iOS location settings target an explicit app

1 participant