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.
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 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"; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 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> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 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> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 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> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 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> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 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. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 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 | )} | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 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.