From a3ee71b27dcb59d6edd8373c3c826f4029cdffc4 Mon Sep 17 00:00:00 2001 From: GautamTalksDev Date: Sun, 30 Aug 2026 03:47:31 -0400 Subject: [PATCH 1/4] 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/4] 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], + }; +} From be4cf4312f9a2f17fbd8b6dc841d165f7d03ba1a Mon Sep 17 00:00:00 2001 From: GautamTalksDev Date: Sun, 30 Aug 2026 04:36:53 -0400 Subject: [PATCH 3/4] docs: add Qodo code review evidence section --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 98128cc..4535f14 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,22 @@ More detail is available in [`docs/HARNESS.md`](docs/HARNESS.md), [`docs/API.md` The default demo is replay based. It verifies the UI, API, decision path, dry run execution, streamed results, and ledger hash verification without making provider calls. The TrueForge driver has been exercised against the local harness and the Keyring MCP endpoints, including the five system fan out, reconciliation, and card persistence. Live GitHub and Google Workspace provider operations remain opt in and require credentials, a configured MCP server, and a throwaway test organization. The repository does not claim that every live provider response or live mutation path has been verified. +## Qodo Code Review Evidence + +Qodo was installed before the first feature commit and reviewed all nine pull requests. Nothing merged to `main` without a review. + +**Representative merged PR:** [#2, audit chain fork regression](https://github.com/GautamTalksDev/keyring/pull/2) + +Qodo found that a regression test permanently altered the `recorded_at` column default and never restored it, leaking a fixed timestamp into every subsequent test on the shared Postgres backend. We wrapped the mutation in `try/finally` so restoration happens even when an assertion fails. The PR history records the completed review, our decision to fix the isolation leak rather than add a retry, and Qodo's follow up review against the final code before merge. + +The deepest fix Qodo prompted was in the audit ledger. A test failed once and passed on retry, but the underlying issue was that two records written in the same millisecond could claim the same parent and fork the hash chain. That meant the tamper-evident ledger was not tamper-evident under concurrent writes. We fixed it with a monotonic sequence column, a unique constraint on the parent hash, and an advisory lock. + +Other merged PRs with substantive Qodo findings, all fixed before merge: + +- [#8, demo and security hardening](https://github.com/GautamTalksDev/keyring/pull/8) fixed five bugs, including a demo card limit applied to production scan paths that silently dropped grants from real audits, and a secret scanner that reported success without scanning anything. +- [#7, guided demo safeguards](https://github.com/GautamTalksDev/keyring/pull/7) fixed four bugs, including guided demo decisions committing server-side after a stop. +- [#9, queue legibility](https://github.com/GautamTalksDev/keyring/pull/9) fixed the UI labelling declared agents as unregistered because it inferred registration from missing attribution instead of reading the authoritative declaration status. + ## AI assistance disclosure This project was developed with assistance from Cursor, an AI coding agent. Humans directed the product decisions, safety defaults, tests, review, and final verification. Cursor assisted with implementation, refactoring, testing, and documentation drafting, as permitted by the hackathon rules. From dec668a004fa449930fd5a66e8596bbe5307ec08 Mon Sep 17 00:00:00 2001 From: GautamTalksDev Date: Sun, 30 Aug 2026 04:50:39 -0400 Subject: [PATCH 4/4] fix(demo): align agent provenance and queue summaries Require self-inventory evidence for the badge, keep guided approvals in visible queue order, and surface unresolved principal warnings by default. Co-authored-by: Cursor --- apps/web/src/components/ApprovalCardView.tsx | 22 ++++++++++++++------ apps/web/src/hooks/useGuidedDemo.ts | 4 ++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/ApprovalCardView.tsx b/apps/web/src/components/ApprovalCardView.tsx index c3606ea..7a22030 100644 --- a/apps/web/src/components/ApprovalCardView.tsx +++ b/apps/web/src/components/ApprovalCardView.tsx @@ -86,10 +86,9 @@ export function ApprovalCardView({ ) : null} {card.grant.principal.kind === "ai_agent" && - (card.grant.principal.agentName === "Keyring" || - card.grant.evidence.some( - (evidence) => evidence.source === "keyring:self-inventory", - )) ? ( + card.grant.evidence.some( + (evidence) => evidence.source === "keyring:self-inventory", + ) ? ( Self-inventory @@ -188,7 +187,9 @@ export function ApprovalCardView({
{inferenceConclusion(card, who)} - ▸ show inference chain + + ▸ show {hasInferenceChain(card) ? "inference chain" : "attribution details"} +

{card.attribution.reasoning} @@ -231,7 +232,12 @@ export function ApprovalCardView({ } function inferenceConclusion(card: ApiCard, who: string): string { - const chain = card.attribution.reasoning.split("Inference chain:")[1]?.trim(); + const chain = hasInferenceChain(card) + ? card.attribution.reasoning.split("Inference chain:")[1]?.trim() + : undefined; + if (!chain && !card.attribution.resolvedTo) { + return `Unattributed · ${card.proposedAction.description}`; + } const firstSignal = chain ?.split(" → ")[0] ?.replace(/^\([^)]+\)\s*/, "") @@ -242,6 +248,10 @@ function inferenceConclusion(card: ApiCard, who: string): string { } · ${firstSignal || "no matching inference"}`; } +function hasInferenceChain(card: ApiCard): boolean { + return card.attribution.reasoning.includes("Inference chain:"); +} + function topRiskReason(reasons: string[]): string { return ( reasons.reduce((top, reason) => { diff --git a/apps/web/src/hooks/useGuidedDemo.ts b/apps/web/src/hooks/useGuidedDemo.ts index 0633f77..0f733ee 100644 --- a/apps/web/src/hooks/useGuidedDemo.ts +++ b/apps/web/src/hooks/useGuidedDemo.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { executeScanStream, fetchAudit, postDecision } from "../api/client.js"; import type { ApiCard, AuditRecord, AuditVerification, ExecuteResult } from "../api/types.js"; import type { AgentActivityState } from "../api/types.js"; -import { sortCards } from "../lib/format.js"; +import { queueSections } from "../lib/format.js"; export type GuidedDemoPhase = | "idle" @@ -259,7 +259,7 @@ export function useGuidedDemo({ }); await wait(HEADLINE_HOLD_MS, controller.signal); - const ordered = sortCards(cardsRef.current); + const ordered = queueSections(cardsRef.current).visualOrder; const safeCards = ordered.filter( (card) => card.status === "pending" &&