Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
22 changes: 16 additions & 6 deletions apps/web/src/components/ApprovalCardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,9 @@ export function ApprovalCardView({
</span>
) : 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",
) ? (
<span className="border border-[var(--color-ink)] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.06em] text-[var(--color-ink)]">
Self-inventory
</span>
Expand Down Expand Up @@ -188,7 +187,9 @@ export function ApprovalCardView({
<details className="mt-2.5 border-t border-[var(--color-line)] pt-2 text-[12px]">
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 text-[var(--color-ink-2)] [&::-webkit-details-marker]:hidden">
<span className="min-w-0 truncate">{inferenceConclusion(card, who)}</span>
<span className="shrink-0 text-[var(--color-faint)]">▸ show inference chain</span>
<span className="shrink-0 text-[var(--color-faint)]">
▸ show {hasInferenceChain(card) ? "inference chain" : "attribution details"}
</span>
</summary>
<p className="mt-2 leading-relaxed text-[var(--color-mute)]">
{card.attribution.reasoning}
Expand Down Expand Up @@ -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*/, "")
Expand All @@ -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<string | null>((top, reason) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/hooks/useGuidedDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" &&
Expand Down
Loading