[GH-ISSUE #545] Misleading permission diagnostics: capture_browser_scenario denies an EMPTY request and reports a 'missing' scope that is present in the ceiling #278

Closed
opened 2026-10-15 18:23:22 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @whft on GitHub (Oct 5, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/545

Summary

capture_browser_scenario fails with permission_required and an actionable-sounding remediation that is factually wrong. The reported missing scope is in fact fully contained in the ceiling shown in the same error payload — including when the request is empty.

This cost significant diagnosis time: the remediation says "Add the exact missing scope beneath the administrator ceiling", which sends you to widen REA_BROWSER_SCENARIO_* configuration that is already correct. The actual blocker is that no grant was ever issued, and there is no CLI command to issue one.

Minimal reproduction

Administrator ceiling configured correctly via environment:

REA_BROWSER_SCENARIO_ENABLED=true
REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON=["C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"]
REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON=["http://127.0.0.1:3000"]
REA_BROWSER_SCENARIO_AUTO_GRANT=  (unset)

rea policy status shows the ceiling is present and correct:

- capability: browser_automate
  executables: ["...\\chrome.exe"]
  origins: ["http://127.0.0.1:3000", ...]
  network: external

Now ask for an empty scope — nothing but the capability name:

$ rea policy explain browser_automate
allowed: false
error:
  code: permission_required
  details:
    capability: browser_automate
    requested:
      executables: []
      environment_names: []
      origins: []
      network: none
      mount: false
    missing:                          # <-- note: empty
    ceiling:
      executables: ["...\\chrome.exe"]
      origins: ["http://127.0.0.1:3000", ...]
      network: external

An empty request with network: none is trivially inside any ceiling. Per missingFromScopes() in src/domain/permissionPolicy.ts, every field resolves to "not missing":

  • roots / executables / environment_names / origins are empty arrays, so .filter() yields nothing
  • network short-circuits on request.network === "none" → undefined
  • mount short-circuits on !request.mount → undefined

so hasMissing({}) is false and the request should be allowed. It is denied instead.

The misleading part

With a real scope, the error reports scope that is present in the ceiling:

$ rea policy explain browser_automate \
    --executable "C:\Program Files\Google\Chrome\Application\chrome.exe" \
    --origins "http://127.0.0.1:3000" --network loopback

requested: { executables: ["...chrome.exe"], origins: ["http://127.0.0.1:3000"], network: loopback }
missing:   { executables: ["...chrome.exe"], origins: ["http://127.0.0.1:3000"], network: loopback }
ceiling:   { executables: ["...chrome.exe", ...], origins: ["http://127.0.0.1:3000", ...], network: external }

Every entry in missing appears verbatim in ceiling. network is reported missing even though the request (loopback) is narrower than the ceiling (external), and networkRank is explicitly designed to accept that (networkRank(scope) >= networkRank(request)).

I verified the containment predicates independently against those exact strings:

relative(ceilingExe, reqExe)              = ""      -> containsAny = true
ceilingOrigins.includes(reqOrigin)        = true
networkRank(external) >= networkRank(loopback) = 2 >= 1 -> true

So the comparison the error is describing would succeed on all three counts. The error payload's missing is not the reason for the denial.

Root cause

Browser automation — unlike passive observation — is not granted automatically. The administrator ceiling is derived from the REA_BROWSER_SCENARIO_* variables, but an administrator grant only exists when REA_BROWSER_SCENARIO_AUTO_GRANT=true, or when a project store issues one (policy status reports project_store: null here).

When no grant exists, the request is denied, but the error is still shaped as a ceiling-mismatch: it fills missing from a ceiling comparison that was never the actual obstacle, and the remediation text hard-codes the ceiling-widening advice.

Two compounding problems:

  1. Wrong diagnosis. missing is populated from a check that cannot explain the denial.
  2. No way to grant from the CLI. rea policy exposes only status | list | explain | revoke. There is no grant action, so AUTO_GRANT=true is the only CLI-reachable route — which is unfortunate, because AUTO_GRANT is documented as appropriate for "a trusted unattended environment", not as the general-purpose way to authorize one local capture.

Setting REA_BROWSER_SCENARIO_AUTO_GRANT=true immediately yields:

$ rea policy explain browser_automate --executable "...chrome.exe" --origins "http://127.0.0.1:3000" --network loopback
allowed: true
grant_id: "administrator:browser_automate"

…and the capture then succeeds end to end.

Suggested fixes

  1. When the denial is "no grant available" rather than "scope exceeds ceiling", say so. Distinguish the two codes, e.g. permission_grant_absent vs outside_administrator_ceiling, and only populate missing in the latter case.
  2. Make the remediation conditional. For a missing grant it should point at REA_BROWSER_SCENARIO_AUTO_GRANT or a project grant, not at widening the ceiling.
  3. Consider adding a rea policy grant action so a single local capture can be authorized without turning on the unattended-environment override.

The ceiling's executables are compared with isPathContained(), so a containing directory is accepted for the executable scope:

executables: ["C:\\Program Files\\Google\\Chrome\\Application"]   -> accepted for ...\\chrome.exe

but the docs and error messages present executables as exact paths, and the scenario request commits an exact executable. Directory-vs-exact semantics are worth documenting explicitly, since the failure mode is silent rejection rather than a clear message.

Verification of the working path

With AUTO_GRANT=true, a launch-mode scenario against a local page exercised goto, fill, check, select_option, press, click, wait_for, and wait_for_timeout:

  • 9 steps, all status: completed
  • completeness.status: complete, equality_eligible: true, missing_sections: []
  • the click genuinely mutated the page (a hidden element's text was captured, and its localStorage counter was observed)
  • per-step URL transitions captured, including fetch targets
  • provider-reported cleanup: terminated-owned-process, process_ownership: provider-owned

So the feature works; only the failure diagnostics are broken.

Environment

REA 3.2.1, Windows 11 x64, Node v24.19.0, Playwright-driven Chromium 154.0.8037.93.

Originally created by @whft on GitHub (Oct 5, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/545 ## Summary `capture_browser_scenario` fails with `permission_required` and an actionable-sounding remediation that is **factually wrong**. The reported `missing` scope is in fact fully contained in the `ceiling` shown in the same error payload — including when the request is *empty*. This cost significant diagnosis time: the remediation says "Add the exact missing scope beneath the administrator ceiling", which sends you to widen `REA_BROWSER_SCENARIO_*` configuration that is already correct. The actual blocker is that **no grant was ever issued**, and there is no CLI command to issue one. ## Minimal reproduction Administrator ceiling configured correctly via environment: ``` REA_BROWSER_SCENARIO_ENABLED=true REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON=["C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"] REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON=["http://127.0.0.1:3000"] REA_BROWSER_SCENARIO_AUTO_GRANT= (unset) ``` `rea policy status` shows the ceiling is present and correct: ``` - capability: browser_automate executables: ["...\\chrome.exe"] origins: ["http://127.0.0.1:3000", ...] network: external ``` Now ask for an **empty** scope — nothing but the capability name: ``` $ rea policy explain browser_automate allowed: false error: code: permission_required details: capability: browser_automate requested: executables: [] environment_names: [] origins: [] network: none mount: false missing: # <-- note: empty ceiling: executables: ["...\\chrome.exe"] origins: ["http://127.0.0.1:3000", ...] network: external ``` An empty request with `network: none` is trivially inside any ceiling. Per `missingFromScopes()` in `src/domain/permissionPolicy.ts`, every field resolves to "not missing": - `roots` / `executables` / `environment_names` / `origins` are empty arrays, so `.filter()` yields nothing - `network` short-circuits on `request.network === "none"` → `undefined` - `mount` short-circuits on `!request.mount` → `undefined` so `hasMissing({})` is `false` and the request should be **allowed**. It is denied instead. ## The misleading part With a real scope, the error reports scope that *is* present in the ceiling: ``` $ rea policy explain browser_automate \ --executable "C:\Program Files\Google\Chrome\Application\chrome.exe" \ --origins "http://127.0.0.1:3000" --network loopback requested: { executables: ["...chrome.exe"], origins: ["http://127.0.0.1:3000"], network: loopback } missing: { executables: ["...chrome.exe"], origins: ["http://127.0.0.1:3000"], network: loopback } ceiling: { executables: ["...chrome.exe", ...], origins: ["http://127.0.0.1:3000", ...], network: external } ``` Every entry in `missing` appears verbatim in `ceiling`. `network` is reported missing even though the request (`loopback`) is *narrower* than the ceiling (`external`), and `networkRank` is explicitly designed to accept that (`networkRank(scope) >= networkRank(request)`). I verified the containment predicates independently against those exact strings: ``` relative(ceilingExe, reqExe) = "" -> containsAny = true ceilingOrigins.includes(reqOrigin) = true networkRank(external) >= networkRank(loopback) = 2 >= 1 -> true ``` So the comparison the error is describing would succeed on all three counts. The error payload's `missing` is not the reason for the denial. ## Root cause Browser automation — unlike passive observation — is not granted automatically. The administrator *ceiling* is derived from the `REA_BROWSER_SCENARIO_*` variables, but an administrator *grant* only exists when `REA_BROWSER_SCENARIO_AUTO_GRANT=true`, or when a project store issues one (`policy status` reports `project_store: null` here). When no grant exists, the request is denied, but the error is still shaped as a ceiling-mismatch: it fills `missing` from a ceiling comparison that was never the actual obstacle, and the remediation text hard-codes the ceiling-widening advice. Two compounding problems: 1. **Wrong diagnosis.** `missing` is populated from a check that cannot explain the denial. 2. **No way to grant from the CLI.** `rea policy` exposes only `status | list | explain | revoke`. There is no `grant` action, so `AUTO_GRANT=true` is the *only* CLI-reachable route — which is unfortunate, because `AUTO_GRANT` is documented as appropriate for "a trusted unattended environment", not as the general-purpose way to authorize one local capture. Setting `REA_BROWSER_SCENARIO_AUTO_GRANT=true` immediately yields: ``` $ rea policy explain browser_automate --executable "...chrome.exe" --origins "http://127.0.0.1:3000" --network loopback allowed: true grant_id: "administrator:browser_automate" ``` …and the capture then succeeds end to end. ## Suggested fixes 1. When the denial is "no grant available" rather than "scope exceeds ceiling", say so. Distinguish the two codes, e.g. `permission_grant_absent` vs `outside_administrator_ceiling`, and only populate `missing` in the latter case. 2. Make the remediation conditional. For a missing grant it should point at `REA_BROWSER_SCENARIO_AUTO_GRANT` or a project grant, not at widening the ceiling. 3. Consider adding a `rea policy grant` action so a single local capture can be authorized without turning on the unattended-environment override. ## Note on a related sharp edge The ceiling's `executables` are compared with `isPathContained()`, so a **containing directory** is accepted for the executable scope: ``` executables: ["C:\\Program Files\\Google\\Chrome\\Application"] -> accepted for ...\\chrome.exe ``` but the docs and error messages present `executables` as exact paths, and the scenario request commits an exact executable. Directory-vs-exact semantics are worth documenting explicitly, since the failure mode is silent rejection rather than a clear message. ## Verification of the working path With `AUTO_GRANT=true`, a launch-mode scenario against a local page exercised `goto`, `fill`, `check`, `select_option`, `press`, `click`, `wait_for`, and `wait_for_timeout`: - 9 steps, all `status: completed` - `completeness.status: complete`, `equality_eligible: true`, `missing_sections: []` - the click genuinely mutated the page (a hidden element's text was captured, and its `localStorage` counter was observed) - per-step URL transitions captured, including `fetch` targets - provider-reported `cleanup: terminated-owned-process`, `process_ownership: provider-owned` So the feature works; only the failure diagnostics are broken. ## Environment REA 3.2.1, Windows 11 x64, Node v24.19.0, Playwright-driven Chromium 154.0.8037.93.
bsakaguchi 2026-10-15 18:23:22 +00:00
Author
Owner

@morluto commented on GitHub (Oct 5, 2026):

Thank you! Will fix it shortly

<!-- gh-comment-id:5991453212 --> @morluto commented on GitHub (Oct 5, 2026): Thank you! Will fix it shortly
Author
Owner

@morluto commented on GitHub (Oct 5, 2026):

Closing as not applicable rather than fixed — the system this report is about no longer exists.

Your diagnosis was correct and worth recording: the denial was reported as a ceiling mismatch when the real blocker was that no grant had ever been issued, and the missing payload was computed against candidate grants (empty) rather than the ceiling, so entries appeared verbatim in both. The suggested split between "grant absent" and "outside ceiling" was the right shape.

That fix briefly existed (#552 added a grant remediation variant so the message would stop blaming the ceiling). It has since been made moot: #555 removed REA's permission grants, scope ceilings, approval confirmations and inert settings outright — PermissionAuthority, PermissionConfiguration, ProjectPermissionStore, permissionPolicy, the policy CLI commands, PermissionRequiredError.remediation, and the filesystem scope inputs are all deleted.

So there is no longer a ceiling to widen and no grant to issue, which makes the misleading diagnostic impossible rather than corrected. Retained as a record of a real diagnostic defect that is now structurally absent.

Also closed as moot for the same reason: #567 (documenting containment semantics of executables) — permissionPolicy.ts and isPathContained no longer exist.

<!-- gh-comment-id:5994982676 --> @morluto commented on GitHub (Oct 5, 2026): Closing as **not applicable** rather than fixed — the system this report is about no longer exists. Your diagnosis was correct and worth recording: the denial was reported as a ceiling mismatch when the real blocker was that no grant had ever been issued, and the `missing` payload was computed against candidate grants (empty) rather than the ceiling, so entries appeared verbatim in both. The suggested split between "grant absent" and "outside ceiling" was the right shape. That fix briefly existed (#552 added a `grant` remediation variant so the message would stop blaming the ceiling). It has since been made **moot**: #555 removed REA's permission grants, scope ceilings, approval confirmations and inert settings outright — `PermissionAuthority`, `PermissionConfiguration`, `ProjectPermissionStore`, `permissionPolicy`, the `policy` CLI commands, `PermissionRequiredError.remediation`, and the filesystem scope inputs are all deleted. So there is no longer a ceiling to widen and no grant to issue, which makes the misleading diagnostic impossible rather than corrected. Retained as a record of a real diagnostic defect that is now structurally absent. Also closed as moot for the same reason: #567 (documenting containment semantics of `executables`) — `permissionPolicy.ts` and `isPathContained` no longer exist.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#278
No description provided.