From a3ee71b27dcb59d6edd8373c3c826f4029cdffc4 Mon Sep 17 00:00:00 2001 From: GautamTalksDev Date: Sun, 30 Aug 2026 03:47:31 -0400 Subject: [PATCH 1/2] feat(ui): collapse inference chains, pin AI agents section Inference chains and risk breakdowns collapse to a one line conclusion with a disclosure control, so cards are scannable at a glance. Adds a dedicated AI agents section above Attributed, with unregistered agents visually emphasised and Keyring's own identity labelled as self inventory. Headline states the unregistered agent count. --- apps/web/src/components/ApprovalCardView.tsx | 76 ++++++++++++++++---- apps/web/src/components/ApprovalQueue.tsx | 49 +++++++++++-- apps/web/src/lib/format.test.ts | 27 +++++++ apps/web/src/lib/format.ts | 18 +++++ 4 files changed, 151 insertions(+), 19 deletions(-) diff --git a/apps/web/src/components/ApprovalCardView.tsx b/apps/web/src/components/ApprovalCardView.tsx index 6e71261..c3606ea 100644 --- a/apps/web/src/components/ApprovalCardView.tsx +++ b/apps/web/src/components/ApprovalCardView.tsx @@ -5,6 +5,7 @@ import { actionVerb, confidenceLabel, formatWhen, + isUnregisteredAgent, principalLabel, staleness, systemLabel, @@ -38,6 +39,7 @@ export function ApprovalCardView({ const stale = staleness(card.grant.lastUsedAt); const pending = card.status === "pending"; const who = principalLabel(card); + const unregisteredAgent = isUnregisteredAgent(card); return (
-

- {card.attribution.reasoning} -

+
+ + {inferenceConclusion(card, who)} + ▸ show inference chain + +

+ {card.attribution.reasoning} +

+
- +
+ + + Top risk factor: {topRiskReason(card.risk.reasons)} + + ▸ show risk breakdown + +
    + {card.risk.reasons.map((r) => ( +
  • + {r} +
  • + ))} +
+
{pending && !actionsDisabled ? (
@@ -207,6 +230,29 @@ export function ApprovalCardView({ ); } +function inferenceConclusion(card: ApiCard, who: string): string { + const chain = card.attribution.reasoning.split("Inference chain:")[1]?.trim(); + const firstSignal = chain + ?.split(" → ")[0] + ?.replace(/^\([^)]+\)\s*/, "") + .split(":")[0] + ?.trim(); + return `${card.attribution.resolvedTo ? `Attributed to ${who}` : "Unattributed"} · ${ + card.attribution.confidence + } · ${firstSignal || "no matching inference"}`; +} + +function topRiskReason(reasons: string[]): string { + return ( + reasons.reduce((top, reason) => { + if (!top) return reason; + const score = Number(reason.match(/\(\+(\d+)\)$/)?.[1] ?? 0); + const topScore = Number(top.match(/\(\+(\d+)\)$/)?.[1] ?? 0); + return score > topScore ? reason : top; + }, null) ?? "no risk factors recorded" + ); +} + function ConfidenceBadge({ confidence }: { confidence: string }) { return ( diff --git a/apps/web/src/components/ApprovalQueue.tsx b/apps/web/src/components/ApprovalQueue.tsx index c9c230e..b183d52 100644 --- a/apps/web/src/components/ApprovalQueue.tsx +++ b/apps/web/src/components/ApprovalQueue.tsx @@ -47,8 +47,13 @@ export function ApprovalQueue({ guidedCardId?: string | null; }) { const ordered = useMemo(() => sortCards(cards), [cards]); - const unattributed = ordered.filter(isUnattributed); - const attributed = ordered.filter((c) => !isUnattributed(c)); + const agents = ordered.filter((card) => card.grant.principal.kind === "ai_agent"); + const unattributed = ordered.filter( + (card) => card.grant.principal.kind !== "ai_agent" && isUnattributed(card), + ); + const attributed = ordered.filter( + (card) => card.grant.principal.kind !== "ai_agent" && !isUnattributed(card), + ); const [focusIndex, setFocusIndex] = useState(0); const [checked, setChecked] = useState>(new Set()); @@ -335,6 +340,42 @@ export function ApprovalQueue({
) : null} + {agents.length > 0 ? ( +
+ +
+ {agents.map((card) => ( + + setFocusIndex( + Math.max( + 0, + focusable.findIndex((c) => c.id === card.id), + ), + ) + } + onToggleCheck={() => toggleCheck(card.id)} + onApprove={() => void decide(card, "approve")} + onHold={() => setHoldTarget(card)} + onReject={() => void decide(card, "reject")} + actionsDisabled={guidedMode} + guidedFocus={guidedCardId === card.id} + /> + ))} +
+
+ ) : null} + {attributed.length > 0 ? (
@@ -416,7 +457,7 @@ function SectionHeading({

{title}

diff --git a/apps/web/src/lib/format.test.ts b/apps/web/src/lib/format.test.ts index 9d35588..61db04c 100644 --- a/apps/web/src/lib/format.test.ts +++ b/apps/web/src/lib/format.test.ts @@ -91,6 +91,7 @@ describe("format helpers", () => { systems: 3, humanIdentities: 2, agentIdentities: 0, + unregisteredAgents: 0, unattributed: 1, overYearIdle: 1, irreversible: 1, @@ -135,6 +136,32 @@ describe("format helpers", () => { const counts = countScanSummary([card({ id: "human" }), agent], ["github", "agent_identity"]); expect(counts.humanIdentities).toBe(1); expect(counts.agentIdentities).toBe(1); + expect(counts.unregisteredAgents).toBe(0); expect(scanSummaryText(counts)).toContain("1 human identity and 1 AI agent identity"); }); + + it("counts unmatched AI agents as unregistered", () => { + const rogue = card({ + id: "rogue", + attribution: { + confidence: "certain", + reasoning: "Agent discovered without a policy match.", + }, + grant: { + ...card({ id: "rogue-base" }).grant, + principal: { + kind: "ai_agent", + agentName: "Unregistered Deployment Agent", + declarationStatus: "declared", + identifiers: [{ kind: "agent_id", value: "rogue", source: "fixture" }], + }, + }, + }); + + const counts = countScanSummary([rogue], ["agent_identity"]); + + expect(counts.agentIdentities).toBe(1); + expect(counts.unregisteredAgents).toBe(1); + expect(scanSummaryText(counts)).toContain("1 unregistered agent."); + }); }); diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index f2b1a2c..1c68cdc 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -7,6 +7,7 @@ export interface ScanSummaryCounts { systems: number; humanIdentities: number; agentIdentities: number; + unregisteredAgents: number; unattributed: number; overYearIdle: number; irreversible: number; @@ -31,11 +32,21 @@ export function countScanSummary( ?.value ?? principalLabel(card), ), ).size; + const unregisteredAgents = new Set( + cards + .filter(isUnregisteredAgent) + .map( + (card) => + card.grant.principal.identifiers.find((identifier) => identifier.kind === "agent_id") + ?.value ?? principalLabel(card), + ), + ).size; return { grants: cards.length, systems: new Set(systemIds).size, humanIdentities, agentIdentities, + unregisteredAgents, unattributed: cards.filter(isUnattributed).length, overYearIdle: cards.filter((card) => staleness(card.grant.lastUsedAt, now).level === "critical") .length, @@ -47,6 +58,9 @@ export function scanSummaryText(counts: ScanSummaryCounts): string { const clauses = [ `${counts.grants} grant${counts.grants === 1 ? "" : "s"} across ${counts.systems} system${counts.systems === 1 ? "" : "s"}.`, `${counts.humanIdentities} human identit${counts.humanIdentities === 1 ? "y" : "ies"} and ${counts.agentIdentities} AI agent identit${counts.agentIdentities === 1 ? "y" : "ies"}.`, + counts.unregisteredAgents > 0 + ? `${counts.unregisteredAgents} unregistered agent${counts.unregisteredAgents === 1 ? "" : "s"}.` + : null, counts.unattributed > 0 ? `${counts.unattributed} we cannot attribute to anyone.` : null, counts.overYearIdle > 0 ? `${counts.overYearIdle} not used in over a year.` : null, counts.irreversible > 0 @@ -76,6 +90,10 @@ export function isUnattributed(card: ApiCard): boolean { return card.attribution.resolvedTo === undefined; } +export function isUnregisteredAgent(card: ApiCard): boolean { + return card.grant.principal.kind === "ai_agent" && card.attribution.resolvedTo === undefined; +} + export function formatWhen(iso: string | null | undefined): string { if (!iso) return "unknown"; const d = new Date(iso); From a8f13878572a405dfbabd6561f7f788df086ffba Mon Sep 17 00:00:00 2001 From: GautamTalksDev Date: Sun, 30 Aug 2026 03:58:56 -0400 Subject: [PATCH 2/2] fix(ui): stop labelling declared agents as unregistered Read principal.declarationStatus instead of inferring registration from missing attribution. Drive keyboard navigation from the rendered queue sections and count agent cards by grants shown. Co-authored-by: Cursor --- apps/web/src/components/ApprovalQueue.tsx | 18 ++- apps/web/src/lib/format.test.ts | 127 +++++++++++++++++++++- apps/web/src/lib/format.ts | 30 ++++- 3 files changed, 161 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/ApprovalQueue.tsx b/apps/web/src/components/ApprovalQueue.tsx index b183d52..96bee77 100644 --- a/apps/web/src/components/ApprovalQueue.tsx +++ b/apps/web/src/components/ApprovalQueue.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { postDecision } from "../api/client.js"; import type { ApiCard } from "../api/types.js"; -import { countScanSummary, isUnattributed, scanSummaryText, sortCards } from "../lib/format.js"; +import { countScanSummary, queueSections, scanSummaryText } from "../lib/format.js"; import { ApprovalCardView } from "./ApprovalCardView.js"; import { ExecutePanel } from "./ExecutePanel.js"; import { HoldDialog } from "./HoldDialog.js"; @@ -46,14 +46,12 @@ export function ApprovalQueue({ guidedMode?: boolean; guidedCardId?: string | null; }) { - const ordered = useMemo(() => sortCards(cards), [cards]); - const agents = ordered.filter((card) => card.grant.principal.kind === "ai_agent"); - const unattributed = ordered.filter( - (card) => card.grant.principal.kind !== "ai_agent" && isUnattributed(card), - ); - const attributed = ordered.filter( - (card) => card.grant.principal.kind !== "ai_agent" && !isUnattributed(card), - ); + const { + unattributed, + agents, + attributed, + visualOrder: ordered, + } = useMemo(() => queueSections(cards), [cards]); const [focusIndex, setFocusIndex] = useState(0); const [checked, setChecked] = useState>(new Set()); @@ -346,7 +344,7 @@ export function ApprovalQueue({ title="AI agents" subtitle="Non-human identities found in connected systems, including Keyring self-inventory." tone="agent" - count={summary.agentIdentities} + count={agents.length} />
{agents.map((card) => ( diff --git a/apps/web/src/lib/format.test.ts b/apps/web/src/lib/format.test.ts index 61db04c..63caa76 100644 --- a/apps/web/src/lib/format.test.ts +++ b/apps/web/src/lib/format.test.ts @@ -4,6 +4,8 @@ import type { ApiCard } from "../api/types.js"; import { countScanSummary, isUnattributed, + isUnregisteredAgent, + queueSections, scanSummaryText, sortCards, staleness, @@ -140,28 +142,147 @@ describe("format helpers", () => { expect(scanSummaryText(counts)).toContain("1 human identity and 1 AI agent identity"); }); - it("counts unmatched AI agents as unregistered", () => { + it("does not label a declared agent as unregistered when attribution is unresolved", () => { + const declaredUnresolved = card({ + id: "declared-unresolved", + attribution: { + confidence: "speculative", + reasoning: "Reconciliation could not resolve this declared agent.", + }, + grant: { + ...card({ id: "declared-unresolved-base" }).grant, + principal: { + kind: "ai_agent", + agentName: "Declared Deployment Agent", + declarationStatus: "declared", + identifiers: [{ kind: "agent_id", value: "declared-1", source: "trueforge" }], + }, + }, + }); + + expect(isUnregisteredAgent(declaredUnresolved)).toBe(false); + expect(isUnattributed(declaredUnresolved)).toBe(true); + + const counts = countScanSummary([declaredUnresolved], ["agent_identity"]); + expect(counts.agentIdentities).toBe(1); + expect(counts.unregisteredAgents).toBe(0); + expect(scanSummaryText(counts)).not.toContain("unregistered agent"); + }); + + it("counts agents whose declarationStatus is unregistered", () => { const rogue = card({ id: "rogue", attribution: { confidence: "certain", reasoning: "Agent discovered without a policy match.", + resolvedTo: "owner-1", }, grant: { ...card({ id: "rogue-base" }).grant, principal: { kind: "ai_agent", agentName: "Unregistered Deployment Agent", - declarationStatus: "declared", + declarationStatus: "unregistered", identifiers: [{ kind: "agent_id", value: "rogue", source: "fixture" }], }, }, }); - const counts = countScanSummary([rogue], ["agent_identity"]); + expect(isUnregisteredAgent(rogue)).toBe(true); + const counts = countScanSummary([rogue], ["agent_identity"]); expect(counts.agentIdentities).toBe(1); expect(counts.unregisteredAgents).toBe(1); expect(scanSummaryText(counts)).toContain("1 unregistered agent."); }); + + it("flattens queue sections in visual order for navigation", () => { + const unattributedHuman = card({ + id: "unattributed-human", + risk: { score: 10, reasons: [] }, + attribution: { confidence: "speculative", reasoning: "unknown bucket" }, + grant: { + ...card({ id: "unattributed-human-base" }).grant, + principal: { kind: "unknown", identifiers: [] }, + }, + }); + const agent = card({ + id: "agent", + risk: { score: 5, reasons: [] }, + attribution: { + confidence: "certain", + reasoning: "TrueForge registration", + resolvedTo: "agent-1", + }, + grant: { + ...card({ id: "agent-base" }).grant, + principal: { + kind: "ai_agent", + agentName: "Keyring", + declarationStatus: "declared", + identifiers: [{ kind: "agent_id", value: "keyring-self", source: "trueforge" }], + }, + }, + }); + const attributedHuman = card({ + id: "attributed-human", + risk: { score: 90, reasons: [] }, + }); + + const sorted = sortCards([attributedHuman, agent, unattributedHuman]).map((c) => c.id); + expect(sorted).toEqual(["unattributed-human", "attributed-human", "agent"]); + + const sections = queueSections([attributedHuman, agent, unattributedHuman]); + expect(sections.visualOrder.map((c) => c.id)).toEqual([ + "unattributed-human", + "agent", + "attributed-human", + ]); + expect(sections.agents).toHaveLength(1); + expect(sections.agents.map((c) => c.id)).toEqual(["agent"]); + }); + + it("counts one queue row per agent grant, not per unique identity", () => { + const first = card({ + id: "agent-grant-1", + attribution: { + confidence: "certain", + reasoning: "TrueForge registration", + resolvedTo: "agent-1", + }, + grant: { + ...card({ id: "agent-grant-1-base" }).grant, + principal: { + kind: "ai_agent", + agentName: "Keyring", + declarationStatus: "declared", + identifiers: [{ kind: "agent_id", value: "keyring-self", source: "trueforge" }], + }, + }, + }); + const second = card({ + id: "agent-grant-2", + attribution: { + confidence: "certain", + reasoning: "TrueForge registration", + resolvedTo: "agent-1", + }, + grant: { + ...card({ id: "agent-grant-2-base" }).grant, + system: "github", + principal: { + kind: "ai_agent", + agentName: "Keyring", + declarationStatus: "declared", + identifiers: [{ kind: "agent_id", value: "keyring-self", source: "trueforge" }], + }, + }, + }); + + const sections = queueSections([first, second]); + const counts = countScanSummary([first, second], ["agent_identity", "github"]); + + expect(counts.agentIdentities).toBe(1); + expect(sections.agents).toHaveLength(2); + }); }); diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index 1c68cdc..fa3e389 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -91,7 +91,10 @@ export function isUnattributed(card: ApiCard): boolean { } export function isUnregisteredAgent(card: ApiCard): boolean { - return card.grant.principal.kind === "ai_agent" && card.attribution.resolvedTo === undefined; + return ( + card.grant.principal.kind === "ai_agent" && + card.grant.principal.declarationStatus === "unregistered" + ); } export function formatWhen(iso: string | null | undefined): string { @@ -168,3 +171,28 @@ export function sortCards(cards: ApiCard[]): ApiCard[] { return b.risk.score - a.risk.score; }); } + +export interface QueueSections { + unattributed: ApiCard[]; + agents: ApiCard[]; + attributed: ApiCard[]; + visualOrder: ApiCard[]; +} + +/** Section order the queue renders: unattributed, agents, attributed. */ +export function queueSections(cards: ApiCard[]): QueueSections { + const ordered = sortCards(cards); + const unattributed = ordered.filter( + (card) => card.grant.principal.kind !== "ai_agent" && isUnattributed(card), + ); + const agents = ordered.filter((card) => card.grant.principal.kind === "ai_agent"); + const attributed = ordered.filter( + (card) => card.grant.principal.kind !== "ai_agent" && !isUnattributed(card), + ); + return { + unattributed, + agents, + attributed, + visualOrder: [...unattributed, ...agents, ...attributed], + }; +}