From ebd1834004a6f0bfe5d60bcd6e462deca1e72e5a Mon Sep 17 00:00:00 2001 From: Hogne <227774406+hognek@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:59:26 +0200 Subject: [PATCH 1/2] fix(notifications): show approve/deny buttons for agent_scope_requests in bell and toast PR #1921 (agent scope-requests) emits notifications with source 'agent_scope_requests' but NotificationCentre and NotificationToast only rendered ConsentActions for source === 'auth_requests'. Scope- request approve/deny was invisible in the UI. - Branch source check in NotificationCentre and NotificationToast to also accept 'agent_scope_requests' - ConsentActions now accepts optional source + canonicalId props and routes approve/deny to the scope-request endpoints when source is 'agent_scope_requests' (/api/agents/registry/{id}/scope-requests/...) - consentPayload() extracts canonical_id from notification data for the scope-request endpoint path - Backward compatible: source defaults to 'auth_requests' for existing callers (DecisionsApp, tests) Ref: #1921 --- desktop/src/components/ConsentActions.tsx | 16 +++++++++++----- desktop/src/components/NotificationCentre.tsx | 4 +++- desktop/src/components/NotificationToast.tsx | 4 +++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/desktop/src/components/ConsentActions.tsx b/desktop/src/components/ConsentActions.tsx index e955c3c45..3a215fc7d 100644 --- a/desktop/src/components/ConsentActions.tsx +++ b/desktop/src/components/ConsentActions.tsx @@ -38,11 +38,15 @@ export function ConsentActions({ scopes, requestedProjectId, onResolved, + source = "auth_requests", + canonicalId, }: { requestId: string; scopes: string[]; requestedProjectId?: string; onResolved?: () => void; + source?: string; + canonicalId?: string; }) { const needsProject = scopes.some((s) => PROJECT_SCOPES.has(s)); const [busy, setBusy] = useState(false); @@ -133,9 +137,10 @@ export function ConsentActions({ setBusy(false); return; } - const url = approved - ? `/api/agents/auth-requests/${encodeURIComponent(requestId)}/approve` - : `/api/agents/auth-requests/${encodeURIComponent(requestId)}/deny`; + const baseUrl = source === "agent_scope_requests" + ? `/api/agents/registry/${encodeURIComponent(canonicalId ?? requestId)}/scope-requests/${encodeURIComponent(requestId)}` + : `/api/agents/auth-requests/${encodeURIComponent(requestId)}`; + const url = `${baseUrl}/${approved ? "approve" : "deny"}`; let body: string | undefined; if (approved) { const payload: { granted_scopes: string[]; project_id?: string } = { granted_scopes: scopes }; @@ -276,12 +281,13 @@ export function ConsentActions({ * is missing. */ export function consentPayload( data: Record | undefined, -): { requestId: string; scopes: string[]; projectId?: string } | null { +): { requestId: string; scopes: string[]; projectId?: string; canonicalId?: string } | null { if (!data) return null; const requestId = data.request_id; if (typeof requestId !== "string") return null; const raw = data.requested_scopes; const scopes = Array.isArray(raw) ? raw.filter((s): s is string => typeof s === "string") : []; const projectId = typeof data.project_id === "string" ? data.project_id : undefined; - return { requestId, scopes, projectId }; + const canonicalId = typeof data.canonical_id === "string" ? data.canonical_id : undefined; + return { requestId, scopes, projectId, canonicalId }; } diff --git a/desktop/src/components/NotificationCentre.tsx b/desktop/src/components/NotificationCentre.tsx index 309c41efa..aef268cb2 100644 --- a/desktop/src/components/NotificationCentre.tsx +++ b/desktop/src/components/NotificationCentre.tsx @@ -39,7 +39,7 @@ function NotificationItem({ }) { // Agent access-requests carry inline Allow/Deny actions. These nested buttons // live outside the clickable row to keep the markup valid. - const consent = n.source === "auth_requests" ? consentPayload(n.data) : null; + const consent = (n.source === "auth_requests" || n.source === "agent_scope_requests") ? consentPayload(n.data) : null; return (
diff --git a/desktop/src/components/NotificationToast.tsx b/desktop/src/components/NotificationToast.tsx index 61982e302..0fdced5c5 100644 --- a/desktop/src/components/NotificationToast.tsx +++ b/desktop/src/components/NotificationToast.tsx @@ -270,7 +270,7 @@ function ToastItem({ notif, onExpire }: { notif: Notification; onExpire: () => v const archiveRead = useNotificationStore((s) => s.archiveRead); const Icon = LEVEL_ICONS[notif.level]; const isAgentPaused = notif.source === "agent.paused"; - const consent = notif.source === "auth_requests" ? consentPayload(notif.data) : null; + const consent = (notif.source === "auth_requests" || notif.source === "agent_scope_requests") ? consentPayload(notif.data) : null; // Stable boolean for the effect dependency — using the `consent` object // would recreate the timer on every render because consentPayload() returns // a new object reference each time. @@ -307,6 +307,8 @@ function ToastItem({ notif, onExpire }: { notif: Notification; onExpire: () => v requestId={consent.requestId} scopes={consent.scopes} requestedProjectId={consent.projectId} + source={notif.source} + canonicalId={consent.canonicalId} onResolved={() => archiveRead(notif.id)} /> )} From de9d6e8c9b374fa1b601d735b2455b70d03b1a1d Mon Sep 17 00:00:00 2001 From: Hogne <227774406+hognek@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:06:50 +0200 Subject: [PATCH 2/2] fix: guard missing canonicalId for scope-request consent actions Per CodeRabbit review: the canonicalId ?? requestId fallback could produce malformed URLs like /registry/{requestId}/scope-requests/... when canonical_id is missing from the notification data. Now explicitly rejects scope-request approve/deny with a clear error when the canonicalId is absent. --- desktop/src/components/ConsentActions.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/desktop/src/components/ConsentActions.tsx b/desktop/src/components/ConsentActions.tsx index 3a215fc7d..0e7a08f8e 100644 --- a/desktop/src/components/ConsentActions.tsx +++ b/desktop/src/components/ConsentActions.tsx @@ -137,8 +137,13 @@ export function ConsentActions({ setBusy(false); return; } + if (source === "agent_scope_requests" && !canonicalId) { + setError("Missing agent identifier; cannot process this scope request."); + setBusy(false); + return; + } const baseUrl = source === "agent_scope_requests" - ? `/api/agents/registry/${encodeURIComponent(canonicalId ?? requestId)}/scope-requests/${encodeURIComponent(requestId)}` + ? `/api/agents/registry/${encodeURIComponent(canonicalId!)}/scope-requests/${encodeURIComponent(requestId)}` : `/api/agents/auth-requests/${encodeURIComponent(requestId)}`; const url = `${baseUrl}/${approved ? "approve" : "deny"}`; let body: string | undefined;