Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 1 | /** |
| 2 | * The audit log's entries, as the workspace's Audit log page, an agent | |
| 3 | * run's page and a pull request's Agent panel show them. | |
| 4 | */ | |
| 5 | ||
| 6 | import { ShieldAlert, ShieldCheck } from "lucide-react"; | |
| 7 | import { useEffect } from "react"; | |
| 8 | import { Link, useFetcher } from "react-router"; | |
| 9 | ||
| 10 | import type { AuditEntry } from "@g1t/contracts"; | |
| 11 | ||
| 12 | import { actionLabel, actorLabel, ruleLabel, targetLabel } from "../lib/audit"; | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 13 | import { TimeAgo } from "./ui"; |
| 14 | import { Avatar } from "./ui/avatar"; | |
| 15 | import { Card } from "./ui/card"; | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 16 | import { Hint } from "./ui/hint"; |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 17 | import { Loading, SkeletonRows } from "./ui/skeleton"; |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 18 | |
| 19 | function clock(at: string): string { | |
| 20 | return new Date(at).toISOString().slice(11, 19); | |
| 21 | } | |
| 22 | ||
| 23 | /** Allowed or denied, and by which rule. */ | |
| 24 | export function OutcomeMark({ entry }: { entry: AuditEntry }) { | |
| 25 | const denied = entry.outcome === "denied"; | |
| 26 | return ( | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 27 | <Hint label={`${entry.outcome}: ${ruleLabel(entry.rule)} (${entry.rule})`}> |
| 28 | <span | |
| 29 | className={`inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs ${ | |
| 30 | denied ? "bg-danger/10 text-danger ring-1 ring-danger/30" : "bg-raised text-muted ring-1 ring-line" | |
| 31 | }`} | |
| 32 | > | |
| 33 | {denied ? <ShieldAlert size={11} /> : <ShieldCheck size={11} />} | |
| 34 | {entry.outcome} | |
| 35 | </span> | |
| 36 | </Hint> | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 37 | ); |
| 38 | } | |
| 39 | ||
| 40 | /** Who acted, with an agent shown as working for someone. */ | |
| 41 | export function ActorLine({ entry }: { entry: AuditEntry }) { | |
| 42 | return ( | |
| 43 | <span className="flex min-w-0 items-center gap-2"> | |
| 44 | <Avatar name={entry.agent ?? entry.actor} size={18} square={entry.actorKind === "workspace"} /> | |
| 45 | <span className="truncate"> | |
| 46 | {entry.onBehalfOf ? ( | |
| 47 | <> | |
| 48 | <span className="font-medium">{entry.agent ?? entry.actor}</span> | |
| 49 | <span className="text-muted"> on behalf of </span> | |
| 50 | <span className="font-medium">{entry.onBehalfOf}</span> | |
| 51 | </> | |
| 52 | ) : ( | |
| 53 | <span className="font-medium">{entry.actor}</span> | |
| 54 | )} | |
| 55 | </span> | |
| 56 | </span> | |
| 57 | ); | |
| 58 | } | |
| 59 | ||
| 60 | /** The workspace's log, one row an entry, newest first. */ | |
| 61 | export function AuditTable({ entries, base }: { entries: AuditEntry[]; base: string }) { | |
| 62 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 63 | <Card asChild divided> |
| 64 | <ol> | |
| 65 | {entries.map((entry) => ( | |
| 66 | <li key={entry.id} className="grid gap-x-4 gap-y-1 px-4 py-3 text-sm sm:grid-cols-[9rem_1fr_auto]"> | |
| 67 | <span className="text-xs leading-5 text-faint"> | |
| 68 | <TimeAgo at={entry.time} /> | |
| 69 | </span> | |
| 70 | <div className="min-w-0"> | |
| 71 | <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1"> | |
| 72 | <ActorLine entry={entry} /> | |
| 73 | <span className="font-mono text-xs text-fg/85">{actionLabel(entry.action)}</span> | |
| 74 | <span className="truncate font-mono text-xs text-muted">{targetLabel(entry)}</span> | |
| 75 | </div> | |
| 76 | <p className="mt-1 flex flex-wrap gap-x-3 text-xs text-faint"> | |
| 77 | <Hint label={entry.rule}> | |
| 78 | <span>{ruleLabel(entry.rule)}</span> | |
| 79 | </Hint> | |
| 80 | <span>{entry.surface.toUpperCase()}</span> | |
| 81 | {entry.result && entry.result !== "ok" && <span>result: {entry.result}</span>} | |
| 82 | {entry.runId && entry.repo && ( | |
| 83 | <Link to={`/${entry.repo}/agents/runs/${entry.runId}`} className="hover:text-fg"> | |
| 84 | {entry.runKind ?? "agent"} run | |
| 85 | </Link> | |
| 86 | )} | |
| 87 | {entry.runId && ( | |
| 88 | <Link to={`${base}?run=${encodeURIComponent(entry.runId)}`} className="hover:text-fg"> | |
| 89 | everything this run did | |
| 90 | </Link> | |
| 91 | )} | |
| 92 | {entry.credentialId && ( | |
| 93 | <Hint label="Credential"> | |
| 94 | <span className="font-mono"> | |
| 95 | <span className="sr-only">Credential </span> | |
| 96 | {entry.credentialId} | |
| 97 | </span> | |
| 98 | </Hint> | |
| 99 | )} | |
| 100 | <Hint label="Request id"> | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 101 | <span className="font-mono"> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 102 | <span className="sr-only">Request id </span> |
| 103 | {entry.requestId} | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 104 | </span> |
| 105 | </Hint> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 106 | </p> |
| 107 | {entry.outcome === "denied" && entry.message && <p className="mt-1 text-xs text-danger">{entry.message}</p>} | |
| 108 | </div> | |
| 109 | <span className="sm:text-right"> | |
| 110 | <OutcomeMark entry={entry} /> | |
| 111 | </span> | |
| 112 | </li> | |
| 113 | ))} | |
| 114 | </ol> | |
| 115 | </Card> | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 116 | ); |
| 117 | } | |
| 118 | ||
| 119 | /** | |
| 120 | * What an agent's run did, oldest first: every call it made and every git | |
| 121 | * request, allowed or refused. `entries` come from the run's audit log. | |
| 122 | */ | |
| 123 | export function WhatItDid({ entries, compact = false }: { entries: AuditEntry[]; compact?: boolean }) { | |
| 124 | if (entries.length === 0) { | |
| 125 | return ( | |
| 126 | <p className="mt-3 text-sm text-muted"> | |
| 127 | Nothing recorded yet. Every call this run makes with its credentials, and every clone and push, is listed here. | |
| 128 | </p> | |
| 129 | ); | |
| 130 | } | |
| 131 | const who = entries.find((entry) => entry.onBehalfOf); | |
| 132 | const denied = entries.filter((entry) => entry.outcome === "denied").length; | |
| 133 | const shown = compact ? entries.slice(-8) : entries; | |
| 134 | return ( | |
| 135 | <div className="mt-3"> | |
| 136 | <p className="text-xs text-muted"> | |
| 137 | {who ? actorLabel(who) : entries[0].actor} · {entries.length} {entries.length === 1 ? "action" : "actions"} | |
| 138 | {denied > 0 && <span className="text-danger"> · {denied} refused</span>} | |
| 139 | {compact && entries.length > shown.length && ` · the latest ${shown.length}`} | |
| 140 | </p> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 141 | <Card asChild divided className="mt-2"> |
| 142 | <ol> | |
| 143 | {shown.map((entry) => ( | |
| 144 | <li key={entry.id} className="flex items-start gap-3 px-4 py-2 text-sm"> | |
| 145 | <time dateTime={entry.time} className="shrink-0 font-mono text-xs leading-5 text-faint" suppressHydrationWarning> | |
| 146 | {clock(entry.time)} | |
| 147 | </time> | |
| 148 | <div className="min-w-0 grow"> | |
| 149 | <p className="flex min-w-0 flex-wrap items-center gap-x-2 font-mono text-xs leading-5"> | |
| 150 | <span className="text-fg/85">{actionLabel(entry.action)}</span> | |
| 151 | <span className="truncate text-muted">{targetLabel(entry)}</span> | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 152 | </p> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 153 | {entry.outcome === "denied" && ( |
| 154 | <p className="text-xs text-danger"> | |
| 155 | {entry.message ?? "Refused."} <span className="text-faint">({ruleLabel(entry.rule)})</span> | |
| 156 | </p> | |
| 157 | )} | |
| 158 | </div> | |
| 159 | <OutcomeMark entry={entry} /> | |
| 160 | </li> | |
| 161 | ))} | |
| 162 | </ol> | |
| 163 | </Card> | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 164 | </div> |
| 165 | ); | |
| 166 | } | |
| 167 | ||
| 168 | /** | |
| 169 | * What the runs on a pull request did, fetched from the project's | |
| 170 | * `audit.json`, for the Agent panel. Shown to members only. | |
| 171 | */ | |
| 172 | export function RunAudit({ owner, repo, runIds, live }: { owner: string; repo: string; runIds: string[]; live: boolean }) { | |
| 173 | const fetcher = useFetcher<{ entries: AuditEntry[] }>(); | |
| 174 | const search = new URLSearchParams(runIds.map((id) => ["run", id])).toString(); | |
| 175 | const url = `/${owner}/${repo}/audit.json?${search}`; | |
| 176 | const { load } = fetcher; | |
| 177 | useEffect(() => { | |
| 178 | if (runIds.length > 0) load(url); | |
| 179 | }, [load, url, runIds.length]); | |
| 180 | useEffect(() => { | |
| 181 | if (!live) return; | |
| 182 | const timer = setInterval(() => { | |
| 183 | if (document.visibilityState === "visible") load(url); | |
| 184 | }, 8000); | |
| 185 | return () => clearInterval(timer); | |
| 186 | }, [live, load, url]); | |
| 187 | const entries = fetcher.data?.entries; | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 188 | if (runIds.length === 0) return null; |
| 189 | // There from the start, so the panel does not grow when the log arrives. | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 190 | return ( |
| 191 | <details className="mt-3 group"> | |
| 192 | <summary className="cursor-pointer text-xs text-muted hover:text-fg">What it did</summary> | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 193 | {entries ? ( |
| 194 | <WhatItDid entries={entries} compact /> | |
| 195 | ) : ( | |
| 196 | <Loading className="mt-3 overflow-hidden rounded-xl border border-line bg-surface"> | |
| 197 | <SkeletonRows rows={3} rowClassName="h-9 px-4" /> | |
| 198 | </Loading> | |
| 199 | )} | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 200 | <Link to={`/${owner}/-/audit?project=${encodeURIComponent(repo)}&kind=agent`} className="mt-2 inline-block text-xs text-muted hover:text-fg"> |
| 201 | Open the audit log | |
| 202 | </Link> | |
| 203 | </details> | |
| 204 | ); | |
| 205 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.