Purpose
Android reports which permission ids a call actually changed under two different field names, and one of them is comma-joined text inside a field typed singular. A consumer that wants the changed ids has to know which target shape produced the response and then split a string.
One Android target legitimately expands to several ids: location is ACCESS_FINE_LOCATION + ACCESS_COARSE_LOCATION, photos resolves to a media candidate at runtime. So "what I asked for" and "what changed" are different facts, and only all currently reports the second one as a list.
Current shape
packages/platform-android/src/settings-permission.ts:
| Path |
Response |
| named target revoke (~L145) |
{ permission: revoked.join(','), priorGrantState, warnings? } |
declared pm target revoke (~L527) |
{ permission: [...values].join(','), priorGrantState, warnings? } |
target all (~L191) |
{ permission: 'all', applied, warnings? } |
| not-requested error details (~L506) |
{ appPackage, permission: values.join(',') } |
Input side, packages/contracts/src/client-settings.ts (~L59) declares the request as permission: PermissionTarget — a single target — while SettingOptions.permissionTarget (packages/contracts/src/settings.ts ~L50, src/daemon/handlers/snapshot-settings.ts ~L30) is a bare string. The wire carries one target; only the platform knows the expansion.
Required behavior
One field for "ids this call changed", named the same on every path:
// named target, multi-id expansion
{ permission: 'location', permissions: ['android.permission.ACCESS_FINE_LOCATION', 'android.permission.ACCESS_COARSE_LOCATION'], priorGrantState: 'granted' }
// target all
{ permission: 'all', permissions: [...], warnings?: [...] }
permissions: string[] carries the ids actually mutated, in the order applied. permission keeps meaning the requested target.
applied folds into permissions; do not keep it as a long-lived alias.
- Error details use
permissions: values instead of a joined string.
- Mirror the shape in the public client types and the versioned help, and update
website/docs/docs/commands.md — it currently documents priorGrantState (L738) without stating the id-list field.
- Add a CHANGELOG entry under Changed.
Before preserving any compatibility alias, run git tag --contains on the introducing commits in a clone with full history (AGENTS.md: unreleased API has no external compatibility obligation). cf1f91da49 introduced the joined permission for revoke; this could not be settled from a shallow clone, so decide it there rather than defaulting to an alias.
Observable completion
- No
.join(',') in a response or error-detail field in settings-permission.ts.
pnpm vitest run packages/platform-android/src/__tests__/settings-permission.test.ts src/commands/capture/settings.test.ts green, with a new test asserting a location deny reports both location ids in permissions.
- The Android provider scenario for permissions asserts the list shape end to end, not a split of
permission.
pnpm check:quick, pnpm check:command-docs, pnpm format clean.
Dependencies
No blocker. Split out of #2363 so a maestro/iOS change did not also rewrite the Android response contract. Related to #2700 but independent: that issue owns skip-vs-abort classification, this one owns the response shape.
Purpose
Android reports which permission ids a call actually changed under two different field names, and one of them is comma-joined text inside a field typed singular. A consumer that wants the changed ids has to know which target shape produced the response and then split a string.
One Android target legitimately expands to several ids:
locationisACCESS_FINE_LOCATION+ACCESS_COARSE_LOCATION,photosresolves to a media candidate at runtime. So "what I asked for" and "what changed" are different facts, and onlyallcurrently reports the second one as a list.Current shape
packages/platform-android/src/settings-permission.ts:{ permission: revoked.join(','), priorGrantState, warnings? }pmtarget revoke (~L527){ permission: [...values].join(','), priorGrantState, warnings? }all(~L191){ permission: 'all', applied, warnings? }{ appPackage, permission: values.join(',') }Input side,
packages/contracts/src/client-settings.ts(~L59) declares the request aspermission: PermissionTarget— a single target — whileSettingOptions.permissionTarget(packages/contracts/src/settings.ts~L50,src/daemon/handlers/snapshot-settings.ts~L30) is a barestring. The wire carries one target; only the platform knows the expansion.Required behavior
One field for "ids this call changed", named the same on every path:
permissions: string[]carries the ids actually mutated, in the order applied.permissionkeeps meaning the requested target.appliedfolds intopermissions; do not keep it as a long-lived alias.permissions: valuesinstead of a joined string.website/docs/docs/commands.md— it currently documentspriorGrantState(L738) without stating the id-list field.Before preserving any compatibility alias, run
git tag --containson the introducing commits in a clone with full history (AGENTS.md: unreleased API has no external compatibility obligation).cf1f91da49introduced the joinedpermissionfor revoke; this could not be settled from a shallow clone, so decide it there rather than defaulting to an alias.Observable completion
.join(',')in a response or error-detail field insettings-permission.ts.pnpm vitest run packages/platform-android/src/__tests__/settings-permission.test.ts src/commands/capture/settings.test.tsgreen, with a new test asserting alocationdeny reports both location ids inpermissions.permission.pnpm check:quick,pnpm check:command-docs,pnpm formatclean.Dependencies
No blocker. Split out of #2363 so a maestro/iOS change did not also rewrite the Android response contract. Related to #2700 but independent: that issue owns skip-vs-abort classification, this one owns the response shape.