flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

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