2026-08-22 -- UICODE-6/UICODE-1: permission-surface integrity
Two Phase 1 findings on the permission UI, fixed together on the CI net.
UICODE-6: a stale Always-allow menu still wrote a permanent allow
Section titled “UICODE-6: a stale Always-allow menu still wrote a permanent allow”The Always-allow GenericMenu callback fires only after the native menu
closes. By then the pending request can have moved on — the other host
answered (inline card and floating window share one request), Y/N hotkeys,
an auto-approve level raise auto-answering, turn end, AbortOpenTurn,
process death, or a second can_use_tool superseding the slot.
AgentHub.RespondToPendingPermission’s request-id guard correctly DROPPED
the wire response (and its CLI-durable updatedPermissions) in every such
case — but PermissionCard.OnAlwaysClicked called
PersistAcceptedRuleIfApplicable BEFORE responding, unconditionally. A
decision the user never completed still permanently widened
PanelSettings.allowedTools, which feeds --allowedTools on every future
spawn and suppresses all future prompts for that tool. The card’s own
comment (“a suggestion captured for request A can never widen permissions
on a later request B”) was true for only one of the two persistence
surfaces.
Fix: RespondToPendingPermission now returns bool — TRUE only when the
decision was applied to the live pending request — and the card persists
the durable rule only on TRUE. One staleness authority for both surfaces;
no second check invented. (A void-return reorder would not have worked:
the hub nulls _pendingPermission before returning on the success path,
so the caller cannot distinguish accepted-from-dropped by re-reading
state.)
Deliberately unchanged: persistence still ignores the CLI suggestion’s
destination (the 2026-08-12 “always allow never stuck” WebFetch fix
depends on panel-side persistence even for session-scoped suggestions);
the only new condition is request liveness. AgentClient.RespondToPermission
still performs no id validation of its own — today every caller either
uses the fresh request or goes through the hub guard; tightening the
client layer would disturb PermissionFlowTests’ exact-transcript pins for
no live exposure, and is left for the CORE-6/HUB-4 FIFO work.
Tests: AgentHubStalePermissionTests — matching id true+writes, stale id
false+no write+pending kept, nothing pending, supersession (old id false /
new id true), null/empty id.
UICODE-1: PermissionWindow.CreateGUI had no re-entry guard
Section titled “UICODE-1: PermissionWindow.CreateGUI had no re-entry guard”RebuildAllOpenPanels (the language-switch rebuild) re-invokes
CreateGUI() directly on every open window of BOTH types, and its doc
comment always claimed the path was “re-entry-safe” — true only for
AgentPanelWindow, which tears down subscriptions/scheduler and
root.Clear()s when _built is already true. PermissionWindow — open
exactly when a permission request is on screen — appended a second
host+PermissionCard under the never-cleared root (duplicate cards, the
stale one frozen in the old language), subscribed AgentHub.Changed a
second time (OnDisable removes only one occurrence, leaking one past
teardown), and abandoned the first scheduled refresh loop.
Fix: mirror AgentPanelWindow’s guard — on re-entry unsubscribe, pause+null
the refresh loop, reset _built, then root.Clear() before rebuilding.
Stylesheets need no special handling: LoadStyleSheets is already
idempotent by asset identity.
Tests: PermissionWindowRebuildTests — headless
ScriptableObject.CreateInstance + repeated CreateGUI() (the
AgentPanelWindowStyleSheetTests pattern): exactly one uap-permwin-root
host after any number of rebuilds, and no throw.