fix(gateway): spurious manual scope-upgrade approval for local clients#96381
fix(gateway): spurious manual scope-upgrade approval for local clients#96381allenz-srp wants to merge 1 commit into
Conversation
A local, same-host operator client (e.g. an in-pod agent CLI) was interrupted with a manual scope-upgrade approval prompt mid-session, even though its initial pairing had been silently auto-approved on the same trusted local connection. It reproduces whenever such a client's first gateway call uses a read-scoped method and a later call needs an admin scope: e.g. an agent that calls cron.list (-> operator.read) before cron.add (-> operator.admin) is baselined at operator.read, so the cron.add trips a operator.read -> operator.admin upgrade that was forced onto the manual-approval path, stalling the agent on /approve. shouldAllowSilentLocalPairing() already treats a local scope-upgrade as silent-eligible (like initial pairing and role-upgrades), but the pairing call-site hard-forced silent:false for every scope-upgrade regardless of locality. Extract the decision into resolveDevicePairingSilent() and honor allowSilentLocalPairing for scope-upgrades too. Boundaries: only LOCAL scope upgrades become silent (allowSilentLocalPairing is false for remote clients, so remote escalations stay on the explicit-approval path); the trusted-CIDR and bootstrap node paths never silence a scope-upgrade. Net effect: a local scope-upgrade now behaves like a local role-upgrade already does.
|
Codex review: needs real behavior proof before merge. Reviewed June 24, 2026, 7:21 AM ET / 11:21 UTC. Summary PR surface: Source +35, Tests +70. Total +105 across 3 files. Reproducibility: yes. for the blocking regression: current main has an integration test for the read-to-admin shared-auth reconnect, and the PR changes the Review metrics: 1 noteworthy metric.
Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Keep scope-upgrades manual for already-paired devices, then fix the reported local cron stall by requesting the right ceiling scope before first pairing or by adding a narrower rule that cannot widen an existing approved scope baseline. Do we have a high-confidence way to reproduce the issue? Yes for the blocking regression: current main has an integration test for the read-to-admin shared-auth reconnect, and the PR changes the Is this the best way to solve the issue? No: applying local silent approval to every scope-upgrade is too broad for the current paired-device baseline invariant. A better fix avoids the lower initial cron scope or proves a narrower non-widening local rule with integration coverage. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against ae06d846faf3. Label changesLabel justifications:
Evidence reviewedPR surface: Source +35, Tests +70. Total +105 across 3 files. View PR surface stats
Security concerns:
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Closing this PR — the approach is the wrong layer to fix ECA-1064. The change flips The real fix for our cron-creation stall is one layer up and needs no gateway change: the in-pod CLI should request the ceiling scope ( Thanks for the reviews. |
What Problem This Solves
Fixes an issue where a local, same-host operator client (e.g. an in-pod agent CLI) is interrupted with a manual "approve scope upgrade" prompt in the middle of a task — even though its initial pairing was already silently auto-approved on the same trusted local connection.
It reproduces whenever such a client's first gateway call uses a read-scoped method and a later call needs an admin scope. Registering scheduled jobs trips it: an agent that calls
cron.list(→operator.read) beforecron.add(→operator.admin) gets its device baselined atoperator.read, so thecron.addrequests anoperator.read → operator.adminupgrade that is forced onto the manual-approval path. The agent then stalls asking the user to run/approve <requestId>before it can finish, with no remote or untrusted party involved.Why This Change Was Made
shouldAllowSilentLocalPairing()already treats a local scope-upgrade as silent-eligible (the same as initial pairing and role-upgrades), but the pairing call-site hard-forcedsilent: falsefor everyscope-upgraderegardless of locality, overriding that intent. The decision is extracted intoresolveDevicePairingSilent(), which honorsallowSilentLocalPairingfor scope-upgrades too.Security boundaries (non-goals): only local scope upgrades become silent —
allowSilentLocalPairingisfalsefor remote clients, so remote escalations stay on the explicit-approval path; the trusted-CIDR and bootstrap node paths never silence a scope-upgrade (they target initial node onboarding, not escalation). Net effect: a local scope-upgrade now behaves exactly like a local role-upgrade already does.User Impact
Local / same-host operator clients (including in-pod agents) can complete an operation that needs a higher scope without a spurious manual approval prompt, provided their initial pairing was already silently approved on the same local connection. Agent flows that register cron jobs no longer stall on
/approve. Remote clients are unaffected — scope escalations from remote still require explicit approval.Evidence
vitest): the affected suite passes, including the newresolveDevicePairingSilentcases across all three gateway vitest projects:Test Files 3 passed (3) / Tests 114 passed (114). Cases cover: local scope-upgrade → silent; remote scope-upgrade → manual; trusted-CIDR / bootstrap never silence a scope-upgrade; non-scope-upgrade reasons fall back to the existing OR; nothing-eligible → manual.oxlintclean on the three changed files;tsc --noEmitreports no type errors in them.scope-upgradereason the helper returns the same value as before (allowSilentLocalPairing || allowSilentTrustedCidrsNodePairing || <bootstrap>); onlyscope-upgradechanges from constantfalsetoallowSilentLocalPairing.cron.addsucceeded — confirming the only blocker was the forced prompt.