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.
| ARTIFACTS.md: R8 (shallow sandbox clones) is built | 1 | /** |
| 2 | * The audit log, as the site shows and exports it: who may see what, the | |
| 3 | * filters a page's address carries, and the CSV and JSON it downloads. | |
| 4 | * Pure, so it can be tested; the server side is in audit.server.ts. | |
| 5 | */ | |
| 6 | ||
| 7 | import type { | |
| 8 | ActorKind, | |
| 9 | AuditEntry, | |
| 10 | AuditOutcome, | |
| 11 | AuditQuery, | |
| 12 | AuditVisibility, | |
| 13 | Role, | |
| 14 | } from "@g1t/contracts"; | |
| 15 | ||
| 16 | /** | |
| 17 | * How much of a workspace's log a viewer sees: an owner everything; a | |
| 18 | * member what was done to the workspace's projects, and what they did or | |
| 19 | * had done for them; anyone else nothing. | |
| 20 | */ | |
| 21 | export function visibilityFor(role: Role | null, username: string): AuditVisibility | null { | |
| 22 | if (role === "owner") return { kind: "all" }; | |
| 23 | if (role === "member") return { kind: "projects", username }; | |
| 24 | return null; | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * The earliest time a workspace's log can be read from, given how many | |
| 29 | * days are kept (7 free, 90 on the plan, or what staff set): the later of | |
| 30 | * what was asked for and the start of the window. | |
| 31 | */ | |
| 32 | export function retainedSince(since: string | null | undefined, days: number, now = Date.now()): string { | |
| 33 | const start = new Date(now - days * 24 * 60 * 60 * 1000).toISOString(); | |
| 34 | return since && since > start ? since : start; | |
| 35 | } | |
| 36 | ||
| 37 | /** The filters a page's address can carry. */ | |
| 38 | export type AuditFilters = { | |
| 39 | actor: string; | |
| 40 | action: string; | |
| 41 | project: string; | |
| 42 | outcome: "" | AuditOutcome; | |
| 43 | kind: "" | ActorKind; | |
| 44 | run: string; | |
| 45 | /** `YYYY-MM-DD`, inclusive. */ | |
| 46 | from: string; | |
| 47 | /** `YYYY-MM-DD`, inclusive. */ | |
| 48 | to: string; | |
| 49 | before: string; | |
| 50 | }; | |
| 51 | ||
| 52 | const OUTCOMES: AuditOutcome[] = ["allowed", "denied"]; | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 53 | // `agent` rows (g1t's own agents, before they were removed) still render, but are no longer a filter. |
| 54 | const KINDS: ActorKind[] = ["person", "workspace"]; | |
| ARTIFACTS.md: R8 (shallow sandbox clones) is built | 55 | const DAY = /^\d{4}-\d{2}-\d{2}$/; |
| 56 | ||
| 57 | function clean(value: string | null, max = 120): string { | |
| 58 | return (value ?? "").trim().slice(0, max); | |
| 59 | } | |
| 60 | ||
| 61 | export function parseFilters(params: URLSearchParams): AuditFilters { | |
| 62 | const outcome = clean(params.get("outcome")); | |
| 63 | const kind = clean(params.get("kind")); | |
| 64 | const day = (name: string) => { | |
| 65 | const value = clean(params.get(name)); | |
| 66 | return DAY.test(value) ? value : ""; | |
| 67 | }; | |
| 68 | return { | |
| 69 | actor: clean(params.get("actor")), | |
| 70 | action: clean(params.get("action")), | |
| 71 | project: clean(params.get("project")), | |
| 72 | outcome: OUTCOMES.includes(outcome as AuditOutcome) ? (outcome as AuditOutcome) : "", | |
| 73 | kind: KINDS.includes(kind as ActorKind) ? (kind as ActorKind) : "", | |
| 74 | run: clean(params.get("run")), | |
| 75 | from: day("from"), | |
| 76 | to: day("to"), | |
| 77 | before: clean(params.get("before")), | |
| 78 | }; | |
| 79 | } | |
| 80 | ||
| 81 | /** The day after `day`, for an inclusive end. */ | |
| 82 | function nextDay(day: string): string { | |
| 83 | const date = new Date(`${day}T00:00:00Z`); | |
| 84 | date.setUTCDate(date.getUTCDate() + 1); | |
| 85 | return date.toISOString().slice(0, 10); | |
| 86 | } | |
| 87 | ||
| 88 | /** What to ask the log for. `project` is a project's name in `workspace`, or `owner/name`. */ | |
| 89 | export function toQuery( | |
| 90 | workspace: string, | |
| 91 | visibility: AuditVisibility, | |
| 92 | filters: AuditFilters, | |
| 93 | limit: number, | |
| 94 | ): AuditQuery { | |
| 95 | const project = filters.project.includes("/") ? filters.project : filters.project ? `${workspace}/${filters.project}` : null; | |
| 96 | return { | |
| 97 | workspace, | |
| 98 | visibility, | |
| 99 | actor: filters.actor || null, | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 100 | agent: null, |
| ARTIFACTS.md: R8 (shallow sandbox clones) is built | 101 | action: filters.action || null, |
| 102 | repo: project, | |
| 103 | outcome: filters.outcome || null, | |
| 104 | actorKind: filters.kind || null, | |
| 105 | runIds: filters.run ? [filters.run] : [], | |
| 106 | since: filters.from ? `${filters.from}T00:00:00.000Z` : null, | |
| 107 | until: filters.to ? `${nextDay(filters.to)}T00:00:00.000Z` : null, | |
| 108 | before: filters.before || null, | |
| 109 | limit, | |
| 110 | }; | |
| 111 | } | |
| 112 | ||
| 113 | /** The address of the same view with `changes` applied, for links. */ | |
| 114 | export function filterHref(base: string, filters: AuditFilters, changes: Partial<AuditFilters> = {}): string { | |
| 115 | const merged = { ...filters, ...changes }; | |
| 116 | const params = new URLSearchParams(); | |
| 117 | for (const [key, value] of Object.entries(merged)) { | |
| 118 | if (value) params.set(key, value); | |
| 119 | } | |
| 120 | const search = params.toString(); | |
| 121 | return search ? `${base}?${search}` : base; | |
| 122 | } | |
| 123 | ||
| 124 | /** Who acted, as people read it: "g1t on behalf of syntaqx". */ | |
| 125 | export function actorLabel(entry: Pick<AuditEntry, "actor" | "agent" | "onBehalfOf">): string { | |
| 126 | return entry.onBehalfOf ? `${entry.agent ?? entry.actor} on behalf of ${entry.onBehalfOf}` : entry.actor; | |
| 127 | } | |
| 128 | ||
| 129 | /** What it was done to: `acme/rocket#12`, with a ref or path when there is one. */ | |
| 130 | export function targetLabel(entry: Pick<AuditEntry, "workspace" | "repo" | "number" | "gitRef" | "path">): string { | |
| 131 | const where = entry.repo ? `${entry.repo}${entry.number != null ? `#${entry.number}` : ""}` : entry.workspace; | |
| 132 | const what = [entry.gitRef, entry.path].filter(Boolean).join(" "); | |
| 133 | return what ? `${where} ${what}` : where; | |
| 134 | } | |
| 135 | ||
| 136 | /** An operation's name as a phrase: `create_issue` is "create issue". */ | |
| 137 | export function actionLabel(action: string): string { | |
| 138 | if (action === "git.push") return "git push"; | |
| 139 | if (action === "git.fetch") return "git fetch"; | |
| 140 | return action.replaceAll("_", " "); | |
| 141 | } | |
| 142 | ||
| 143 | export const CSV_COLUMNS = [ | |
| 144 | "id", | |
| 145 | "time", | |
| 146 | "workspace", | |
| 147 | "actorKind", | |
| 148 | "actor", | |
| 149 | "agent", | |
| 150 | "onBehalfOf", | |
| 151 | "runId", | |
| 152 | "runKind", | |
| 153 | "credentialId", | |
| 154 | "action", | |
| 155 | "surface", | |
| 156 | "repo", | |
| 157 | "number", | |
| 158 | "gitRef", | |
| 159 | "path", | |
| 160 | "outcome", | |
| 161 | "rule", | |
| 162 | "result", | |
| 163 | "message", | |
| 164 | "requestId", | |
| 165 | ] as const satisfies readonly (keyof AuditEntry)[]; | |
| 166 | ||
| 167 | function csvCell(value: unknown): string { | |
| 168 | if (value == null) return ""; | |
| 169 | let text = String(value); | |
| 170 | // A spreadsheet runs a cell that starts like a formula; keep it text. | |
| 171 | if (/^[=+\-@\t\r]/.test(text)) text = `'${text}`; | |
| 172 | return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text; | |
| 173 | } | |
| 174 | ||
| 175 | /** RFC 4180 CSV, a header row and one row per entry. */ | |
| 176 | export function toCsv(entries: AuditEntry[]): string { | |
| 177 | const rows = [CSV_COLUMNS.join(",")]; | |
| 178 | for (const entry of entries) rows.push(CSV_COLUMNS.map((column) => csvCell(entry[column])).join(",")); | |
| 179 | return `${rows.join("\r\n")}\r\n`; | |
| 180 | } | |
| 181 | ||
| 182 | /** The download's file name: `acme-audit-2026-10-04.csv`. */ | |
| 183 | export function exportName(workspace: string, format: "csv" | "json", now: Date): string { | |
| 184 | return `${workspace}-audit-${now.toISOString().slice(0, 10)}.${format}`; | |
| 185 | } | |
| 186 | ||
| 187 | /** Plain words for the rule that decided an entry. */ | |
| 188 | export function ruleLabel(rule: string): string { | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 189 | if (rule === "never") return "never allowed for agents (an old entry)"; |
| ARTIFACTS.md: R8 (shallow sandbox clones) is built | 190 | if (rule === "scope:operation") return "not in the run's scope"; |
| 191 | if (rule === "scope:repository") return "outside the run's repository"; | |
| 192 | if (rule === "scope:pull") return "outside the run's pull request"; | |
| 193 | if (rule === "on-behalf-of:membership") return "the person it works for is not a member"; | |
| 194 | if (rule === "git:push") return "no push grant"; | |
| 195 | if (rule === "git:read") return "no read grant"; | |
| 196 | if (rule === "git:ref") return "branch not granted"; | |
| 197 | if (rule === "git:not-a-run") return "tools token used with git"; | |
| 198 | if (rule === "service" || rule === "repository") return "refused by the repository's rules"; | |
| 199 | if (rule === "person") return "the person's own access"; | |
| 200 | if (rule === "workspace-token") return "the workspace token's access"; | |
| 201 | const run = /^run:([a-z]+)\/(runner|tools)(?::(push|read))?$/.exec(rule); | |
| 202 | if (run) return `${run[1]} run ${run[2] === "tools" ? "tools" : "runner"}${run[3] ? ` (${run[3]})` : ""}`; | |
| 203 | return rule; | |
| 204 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.