Skip to content

g1t/apps/web/app/components/audit.tsx

199 lines8,159 bytesCodeBlame

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 API1/**
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
6import { ShieldAlert, ShieldCheck } from "lucide-react";
7import { useEffect } from "react";
8import { Link, useFetcher } from "react-router";
9
10import type { AuditEntry } from "@g1t/contracts";
11
12import { actionLabel, actorLabel, ruleLabel, targetLabel } from "../lib/audit";
13import { Avatar, TimeAgo } from "./ui";
Merge branch 'worktree-agent-ae1299e92e4462012'14import { Hint } from "./ui/hint";
Merge branch 'worktree-agent-aca4790ea223aa235'15import { Loading, SkeletonRows } from "./ui/skeleton";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API16
17function clock(at: string): string {
18 return new Date(at).toISOString().slice(11, 19);
19}
20
21/** Allowed or denied, and by which rule. */
22export function OutcomeMark({ entry }: { entry: AuditEntry }) {
23 const denied = entry.outcome === "denied";
24 return (
Merge branch 'worktree-agent-ae1299e92e4462012'25 <Hint label={`${entry.outcome}: ${ruleLabel(entry.rule)} (${entry.rule})`}>
26 <span
27 className={`inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs ${
28 denied ? "bg-danger/10 text-danger ring-1 ring-danger/30" : "bg-raised text-muted ring-1 ring-line"
29 }`}
30 >
31 {denied ? <ShieldAlert size={11} /> : <ShieldCheck size={11} />}
32 {entry.outcome}
33 </span>
34 </Hint>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API35 );
36}
37
38/** Who acted, with an agent shown as working for someone. */
39export function ActorLine({ entry }: { entry: AuditEntry }) {
40 return (
41 <span className="flex min-w-0 items-center gap-2">
42 <Avatar name={entry.agent ?? entry.actor} size={18} square={entry.actorKind === "workspace"} />
43 <span className="truncate">
44 {entry.onBehalfOf ? (
45 <>
46 <span className="font-medium">{entry.agent ?? entry.actor}</span>
47 <span className="text-muted"> on behalf of </span>
48 <span className="font-medium">{entry.onBehalfOf}</span>
49 </>
50 ) : (
51 <span className="font-medium">{entry.actor}</span>
52 )}
53 </span>
54 </span>
55 );
56}
57
58/** The workspace's log, one row an entry, newest first. */
59export function AuditTable({ entries, base }: { entries: AuditEntry[]; base: string }) {
60 return (
61 <ol className="divide-y divide-line rounded-xl border border-line bg-surface">
62 {entries.map((entry) => (
63 <li key={entry.id} className="grid gap-x-4 gap-y-1 px-4 py-3 text-sm sm:grid-cols-[9rem_1fr_auto]">
64 <span className="text-xs leading-5 text-faint">
65 <TimeAgo at={entry.time} />
66 </span>
67 <div className="min-w-0">
68 <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
69 <ActorLine entry={entry} />
70 <span className="font-mono text-xs text-fg/85">{actionLabel(entry.action)}</span>
71 <span className="truncate font-mono text-xs text-muted">{targetLabel(entry)}</span>
72 </div>
73 <p className="mt-1 flex flex-wrap gap-x-3 text-xs text-faint">
Merge branch 'worktree-agent-ae1299e92e4462012'74 <Hint label={entry.rule}>
75 <span>{ruleLabel(entry.rule)}</span>
76 </Hint>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API77 <span>{entry.surface.toUpperCase()}</span>
78 {entry.result && entry.result !== "ok" && <span>result: {entry.result}</span>}
79 {entry.runId && entry.repo && (
80 <Link to={`/${entry.repo}/agents/runs/${entry.runId}`} className="hover:text-fg">
81 {entry.runKind ?? "agent"} run
82 </Link>
83 )}
84 {entry.runId && (
85 <Link to={`${base}?run=${encodeURIComponent(entry.runId)}`} className="hover:text-fg">
86 everything this run did
87 </Link>
88 )}
89 {entry.credentialId && (
Merge branch 'worktree-agent-ae1299e92e4462012'90 <Hint label="Credential">
91 <span className="font-mono">
92 <span className="sr-only">Credential </span>
93 {entry.credentialId}
94 </span>
95 </Hint>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API96 )}
Merge branch 'worktree-agent-ae1299e92e4462012'97 <Hint label="Request id">
98 <span className="font-mono">
99 <span className="sr-only">Request id </span>
100 {entry.requestId}
101 </span>
102 </Hint>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API103 </p>
104 {entry.outcome === "denied" && entry.message && <p className="mt-1 text-xs text-danger">{entry.message}</p>}
105 </div>
106 <span className="sm:text-right">
107 <OutcomeMark entry={entry} />
108 </span>
109 </li>
110 ))}
111 </ol>
112 );
113}
114
115/**
116 * What an agent's run did, oldest first: every call it made and every git
117 * request, allowed or refused. `entries` come from the run's audit log.
118 */
119export function WhatItDid({ entries, compact = false }: { entries: AuditEntry[]; compact?: boolean }) {
120 if (entries.length === 0) {
121 return (
122 <p className="mt-3 text-sm text-muted">
123 Nothing recorded yet. Every call this run makes with its credentials, and every clone and push, is listed here.
124 </p>
125 );
126 }
127 const who = entries.find((entry) => entry.onBehalfOf);
128 const denied = entries.filter((entry) => entry.outcome === "denied").length;
129 const shown = compact ? entries.slice(-8) : entries;
130 return (
131 <div className="mt-3">
132 <p className="text-xs text-muted">
133 {who ? actorLabel(who) : entries[0].actor} · {entries.length} {entries.length === 1 ? "action" : "actions"}
134 {denied > 0 && <span className="text-danger"> · {denied} refused</span>}
135 {compact && entries.length > shown.length && ` · the latest ${shown.length}`}
136 </p>
137 <ol className="mt-2 divide-y divide-line rounded-xl border border-line bg-surface">
138 {shown.map((entry) => (
139 <li key={entry.id} className="flex items-start gap-3 px-4 py-2 text-sm">
140 <time dateTime={entry.time} className="shrink-0 font-mono text-xs leading-5 text-faint" suppressHydrationWarning>
141 {clock(entry.time)}
142 </time>
143 <div className="min-w-0 grow">
144 <p className="flex min-w-0 flex-wrap items-center gap-x-2 font-mono text-xs leading-5">
145 <span className="text-fg/85">{actionLabel(entry.action)}</span>
146 <span className="truncate text-muted">{targetLabel(entry)}</span>
147 </p>
148 {entry.outcome === "denied" && (
149 <p className="text-xs text-danger">
150 {entry.message ?? "Refused."} <span className="text-faint">({ruleLabel(entry.rule)})</span>
151 </p>
152 )}
153 </div>
154 <OutcomeMark entry={entry} />
155 </li>
156 ))}
157 </ol>
158 </div>
159 );
160}
161
162/**
163 * What the runs on a pull request did, fetched from the project's
164 * `audit.json`, for the Agent panel. Shown to members only.
165 */
166export function RunAudit({ owner, repo, runIds, live }: { owner: string; repo: string; runIds: string[]; live: boolean }) {
167 const fetcher = useFetcher<{ entries: AuditEntry[] }>();
168 const search = new URLSearchParams(runIds.map((id) => ["run", id])).toString();
169 const url = `/${owner}/${repo}/audit.json?${search}`;
170 const { load } = fetcher;
171 useEffect(() => {
172 if (runIds.length > 0) load(url);
173 }, [load, url, runIds.length]);
174 useEffect(() => {
175 if (!live) return;
176 const timer = setInterval(() => {
177 if (document.visibilityState === "visible") load(url);
178 }, 8000);
179 return () => clearInterval(timer);
180 }, [live, load, url]);
181 const entries = fetcher.data?.entries;
Merge branch 'worktree-agent-aca4790ea223aa235'182 if (runIds.length === 0) return null;
183 // 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 API184 return (
185 <details className="mt-3 group">
186 <summary className="cursor-pointer text-xs text-muted hover:text-fg">What it did</summary>
Merge branch 'worktree-agent-aca4790ea223aa235'187 {entries ? (
188 <WhatItDid entries={entries} compact />
189 ) : (
190 <Loading className="mt-3 overflow-hidden rounded-xl border border-line bg-surface">
191 <SkeletonRows rows={3} rowClassName="h-9 px-4" />
192 </Loading>
193 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API194 <Link to={`/${owner}/-/audit?project=${encodeURIComponent(repo)}&kind=agent`} className="mt-2 inline-block text-xs text-muted hover:text-fg">
195 Open the audit log
196 </Link>
197 </details>
198 );
199}

This file's history is long; its oldest lines are credited to the oldest commit read.