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 | * Guardrails: the form a workspace sets its defaults with and a project its | |
| 3 | * overrides, and what a run shows of its caps. | |
| 4 | */ | |
| 5 | import { Clock, Coins, ShieldCheck } from "lucide-react"; | |
| 6 | import { type ReactNode, useEffect, useState } from "react"; | |
| 7 | import { Form } from "react-router"; | |
| 8 | ||
| 9 | import { | |
| 10 | type AgentRun, | |
| 11 | type GuardrailSettings, | |
| 12 | type Guardrails, | |
| 13 | type GuardrailsView, | |
| 14 | RUN_KINDS, | |
| 15 | RUN_KIND_LABEL, | |
| 16 | isActiveRun, | |
| 17 | } from "@g1t/contracts"; | |
| 18 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 19 | import { formatCap, tri, workflowDomainLine } from "../lib/guardrails"; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 20 | import { formatCost } from "./agents"; |
| Merge branch 'worktree-agent-a985c01f43538af01' | 21 | import { ErrorText, Input, SubmitButton, TimeAgo } from "./ui"; |
| 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. | 22 | import { Badge } from "./ui/badge"; |
| 23 | import { Card } from "./ui/card"; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 24 | import { CheckboxOption } from "./ui/checkbox"; |
| 25 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| 26 | import { Textarea } from "./ui/textarea"; | |
| 27 | ||
| 28 | function Section({ title, about, children }: { title: string; about: ReactNode; children: ReactNode }) { | |
| 29 | return ( | |
| 30 | <section className="grid gap-x-10 gap-y-4 border-t border-line pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[16rem_1fr]"> | |
| 31 | <div> | |
| 32 | <h2 className="font-medium">{title}</h2> | |
| 33 | <div className="mt-1 text-sm text-muted">{about}</div> | |
| 34 | </div> | |
| 35 | <div className="min-w-0 space-y-3">{children}</div> | |
| 36 | </section> | |
| 37 | ); | |
| 38 | } | |
| 39 | ||
| 40 | /** A choice of inheriting, or on or off, for one setting. */ | |
| 41 | function TriSelect({ | |
| 42 | name, | |
| 43 | value, | |
| 44 | inherited, | |
| 45 | parent, | |
| 46 | labels, | |
| 47 | title, | |
| 48 | children, | |
| 49 | }: { | |
| 50 | name: string; | |
| 51 | value: boolean | null | undefined; | |
| 52 | inherited: boolean; | |
| 53 | parent: string; | |
| 54 | labels: [on: string, off: string]; | |
| 55 | title: string; | |
| 56 | children: ReactNode; | |
| 57 | }) { | |
| 58 | 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. | 59 | <Card className="flex flex-col gap-3 p-4 sm:flex-row sm:items-start"> |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 60 | <div className="min-w-0 grow"> |
| 61 | <p className="text-sm font-medium">{title}</p> | |
| 62 | <p className="mt-1 text-sm text-muted">{children}</p> | |
| 63 | </div> | |
| 64 | <Select name={name} defaultValue={tri(value)}> | |
| 65 | <SelectTrigger size="sm" aria-label={title} className="w-full shrink-0 sm:w-auto sm:min-w-44"> | |
| 66 | <SelectValue /> | |
| 67 | </SelectTrigger> | |
| 68 | <SelectContent align="end"> | |
| 69 | <SelectItem value="inherit"> | |
| 70 | As {parent} ({(inherited ? labels[0] : labels[1]).toLowerCase()}) | |
| 71 | </SelectItem> | |
| 72 | <SelectItem value="on">{labels[0]}</SelectItem> | |
| 73 | <SelectItem value="off">{labels[1]}</SelectItem> | |
| 74 | </SelectContent> | |
| 75 | </Select> | |
| 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. | 76 | </Card> |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 77 | ); |
| 78 | } | |
| 79 | ||
| 80 | /** Hosts in a compact list. */ | |
| 81 | function Hosts({ hosts }: { hosts: string[] }) { | |
| 82 | return ( | |
| 83 | <span className="font-mono text-xs break-words text-faint">{hosts.length ? hosts.join(", ") : "none"}</span> | |
| 84 | ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * One level's guardrails. `level` says which: the workspace's defaults, | |
| 89 | * over g1t's, or a project's overrides, over its workspace's. | |
| 90 | */ | |
| 91 | export function GuardrailsForm({ | |
| 92 | view, | |
| 93 | level, | |
| 94 | editable, | |
| 95 | saving, | |
| 96 | saved, | |
| 97 | error, | |
| 98 | }: { | |
| 99 | view: GuardrailsView; | |
| 100 | level: "workspace" | "project"; | |
| 101 | editable: boolean; | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 102 | /** Working for a reason the Save button cannot see; it follows its own submission by itself. */ |
| 103 | saving?: boolean; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 104 | saved: boolean; |
| 105 | error: string | null | undefined; | |
| 106 | }) { | |
| 107 | const own: GuardrailSettings = (level === "project" ? view.project : view.workspace) ?? {}; | |
| 108 | // What this level inherits, and what it is called. | |
| 109 | const base: Guardrails = level === "project" ? view.inherited : view.defaults; | |
| 110 | const parent = level === "project" ? "the workspace" : "g1t's default"; | |
| 111 | const [registryMode, setRegistryMode] = useState(own.registries ? "custom" : "inherit"); | |
| 112 | const shownRegistries = own.registries ?? base.registries; | |
| 113 | const inheritedDomains = level === "project" ? view.inherited.domains : []; | |
| 114 | const inheritedDeny = level === "project" ? view.inherited.deny : []; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 115 | const inheritedWorkflowDomains = level === "project" ? (view.inherited.workflowDomains ?? []) : []; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 116 | return ( |
| Merge branch 'worktree-agent-a985c01f43538af01' | 117 | // Keyed to the last change, so after a save the fields show what was kept, as it was tidied. |
| 118 | <Form key={own.updatedAt ?? "unset"} method="post" className="max-w-4xl space-y-8"> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 119 | <fieldset disabled={!editable} className="min-w-0 space-y-8"> |
| 120 | <Section | |
| 121 | title="Network" | |
| 122 | about={ | |
| 123 | <> | |
| 124 | Which hosts a sandbox may reach. Requests anywhere else are refused at the sandbox's edge, and | |
| 125 | each refused host shows on the run as a step. | |
| 126 | </> | |
| 127 | } | |
| 128 | > | |
| 129 | <TriSelect | |
| 130 | name="restrictNetwork" | |
| 131 | value={own.restrictNetwork} | |
| 132 | inherited={base.restrictNetwork} | |
| 133 | parent={parent} | |
| 134 | labels={["Restricted", "Open"]} | |
| 135 | title="Only allowed hosts" | |
| 136 | > | |
| 137 | Restricted, a sandbox reaches g1t, the registries below and the domains you list, over HTTP and | |
| 138 | HTTPS only. Open, it reaches the whole internet. | |
| 139 | </TriSelect> | |
| 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. | 140 | <Card className="p-4"> |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 141 | <p className="text-sm font-medium">Always allowed</p> |
| 142 | <p className="mt-1 text-sm text-muted"> | |
| 143 | g1t's own hosts, for cloning, pushing, reporting and the model: <Hosts hosts={view.g1tHosts} /> | |
| 144 | </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. | 145 | </Card> |
| 146 | <Card className="p-4"> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 147 | <div className="flex flex-col gap-3 sm:flex-row sm:items-start"> |
| 148 | <div className="min-w-0 grow"> | |
| 149 | <p className="text-sm font-medium">Package registries</p> | |
| 150 | <p className="mt-1 text-sm text-muted">Where installs fetch dependencies from.</p> | |
| 151 | </div> | |
| 152 | <Select name="registries" value={registryMode} onValueChange={setRegistryMode}> | |
| 153 | <SelectTrigger size="sm" aria-label="Package registries" className="w-full shrink-0 sm:w-auto sm:min-w-44"> | |
| 154 | <SelectValue /> | |
| 155 | </SelectTrigger> | |
| 156 | <SelectContent align="end"> | |
| 157 | <SelectItem value="inherit">As {parent}</SelectItem> | |
| 158 | <SelectItem value="custom">Choose</SelectItem> | |
| 159 | </SelectContent> | |
| 160 | </Select> | |
| 161 | </div> | |
| 162 | <div className="mt-4 grid gap-3 sm:grid-cols-2"> | |
| 163 | {view.registries.map((registry) => ( | |
| 164 | <CheckboxOption | |
| 165 | key={`${registry.id}-${registryMode}`} | |
| 166 | name={`registry:${registry.id}`} | |
| 167 | disabled={registryMode !== "custom"} | |
| 168 | defaultChecked={(registryMode === "custom" ? shownRegistries : base.registries).includes(registry.id)} | |
| 169 | label={registry.name} | |
| 170 | description={registry.hosts.join(", ")} | |
| 171 | /> | |
| 172 | ))} | |
| 173 | </div> | |
| 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. | 174 | </Card> |
| 175 | <Card className="p-4"> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 176 | <label htmlFor="guardrail-domains" className="text-sm font-medium"> |
| 177 | Allowed domains | |
| 178 | </label> | |
| 179 | <p className="mt-1 text-sm text-muted"> | |
| 180 | One per line: <span className="font-mono text-xs">api.stripe.com</span>, or{" "} | |
| 181 | <span className="font-mono text-xs">*.example.com</span> for its subdomains. | |
| 182 | {level === "project" && " These add to the workspace's."} | |
| 183 | </p> | |
| 184 | <Textarea | |
| 185 | id="guardrail-domains" | |
| 186 | name="domains" | |
| 187 | className="mt-3 font-mono text-xs" | |
| 188 | rows={4} | |
| 189 | defaultValue={(own.domains ?? []).join("\n")} | |
| 190 | placeholder="api.stripe.com" | |
| 191 | /> | |
| 192 | {inheritedDomains.length > 0 && ( | |
| 193 | <p className="mt-2 text-xs text-faint"> | |
| 194 | From the workspace: <Hosts hosts={inheritedDomains} /> | |
| 195 | </p> | |
| 196 | )} | |
| 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. | 197 | </Card> |
| 198 | <Card className="p-4"> | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 199 | <label htmlFor="guardrail-workflow-domains" className="text-sm font-medium"> |
| 200 | Workflow-only domains | |
| 201 | </label> | |
| 202 | <p className="mt-1 text-sm text-muted"> | |
| 203 | Hosts that only workflow jobs may reach, never agents, such as the API a deploy uploads to. One | |
| 204 | per line: the domain, then the workflows and the environments it is for, each comma-separated; | |
| 205 | leave either out for any. Only jobs of runs that are not pull requests from forks get them. | |
| 206 | {level === "project" && " These add to the workspace's."} | |
| 207 | </p> | |
| 208 | <Textarea | |
| 209 | id="guardrail-workflow-domains" | |
| 210 | name="workflowDomains" | |
| 211 | className="mt-3 font-mono text-xs" | |
| 212 | rows={3} | |
| 213 | defaultValue={(own.workflowDomains ?? []).map(workflowDomainLine).join("\n")} | |
| 214 | placeholder="api.cloudflare.com | deploy.yml | production" | |
| 215 | /> | |
| 216 | {inheritedWorkflowDomains.length > 0 && ( | |
| 217 | <p className="mt-2 text-xs text-faint"> | |
| 218 | From the workspace:{" "} | |
| 219 | <span className="font-mono break-words">{inheritedWorkflowDomains.map(workflowDomainLine).join("; ")}</span> | |
| 220 | </p> | |
| 221 | )} | |
| 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. | 222 | </Card> |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 223 | </Section> |
| 224 | ||
| 225 | <Section | |
| 226 | title="Commands" | |
| 227 | about={ | |
| 228 | <> | |
| 229 | What the agent's harness refuses to run. A refused command is not run; the agent is told why, and | |
| 230 | the run shows it as a step. | |
| 231 | </> | |
| 232 | } | |
| 233 | > | |
| 234 | {view.rules.map((rule) => ( | |
| 235 | <TriSelect | |
| 236 | key={rule.id} | |
| 237 | name={`rule:${rule.id}`} | |
| 238 | value={own.rules?.[rule.id]} | |
| 239 | inherited={base.rules[rule.id] ?? true} | |
| 240 | parent={parent} | |
| 241 | labels={["On", "Off"]} | |
| 242 | title={rule.title} | |
| 243 | > | |
| 244 | {rule.about} | |
| 245 | </TriSelect> | |
| 246 | ))} | |
| 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. | 247 | <Card className="p-4"> |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 248 | <label htmlFor="guardrail-deny" className="text-sm font-medium"> |
| 249 | Also refuse | |
| 250 | </label> | |
| 251 | <p className="mt-1 text-sm text-muted"> | |
| 252 | One rule per line, as <span className="font-mono text-xs">Bash(terraform apply:*)</span>,{" "} | |
| 253 | <span className="font-mono text-xs">Edit(//etc/**)</span> or{" "} | |
| 254 | <span className="font-mono text-xs">WebFetch</span>. Plain text is the start of a shell command. | |
| 255 | {level === "project" && " These add to the workspace's."} | |
| 256 | </p> | |
| 257 | <Textarea | |
| 258 | id="guardrail-deny" | |
| 259 | name="deny" | |
| 260 | className="mt-3 font-mono text-xs" | |
| 261 | rows={4} | |
| 262 | defaultValue={(own.deny ?? []).join("\n")} | |
| 263 | placeholder="Bash(terraform apply:*)" | |
| 264 | /> | |
| 265 | {inheritedDeny.length > 0 && ( | |
| 266 | <p className="mt-2 text-xs text-faint"> | |
| 267 | From the workspace: <span className="font-mono">{inheritedDeny.join(", ")}</span> | |
| 268 | </p> | |
| 269 | )} | |
| 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. | 270 | </Card> |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 271 | </Section> |
| 272 | ||
| 273 | <Section | |
| 274 | title="Caps" | |
| 275 | about="How much one run may cost and how long it may take. A run that reaches either is stopped, and its pull request waits for you." | |
| 276 | > | |
| 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. | 277 | <Card className="flex flex-col gap-3 p-4 sm:flex-row sm:items-start"> |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 278 | <div className="min-w-0 grow"> |
| 279 | <label htmlFor="guardrail-budget" className="text-sm font-medium"> | |
| 280 | Cost per run, in US dollars | |
| 281 | </label> | |
| 282 | <p className="mt-1 text-sm text-muted"> | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 283 | Empty: as {parent} ({formatCap(base.budgetUsd)}). 0: no cap here. The workspace's spend cap per run, under Billing, applies as well, and a run stops at the lower of the two. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 284 | </p> |
| 285 | </div> | |
| 286 | <Input | |
| 287 | id="guardrail-budget" | |
| 288 | name="budgetUsd" | |
| 289 | inputMode="decimal" | |
| 290 | className="w-full shrink-0 sm:w-32" | |
| 291 | defaultValue={own.budgetUsd == null ? "" : String(own.budgetUsd)} | |
| 292 | placeholder={base.budgetUsd == null ? "0" : base.budgetUsd.toFixed(2)} | |
| 293 | /> | |
| 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. | 294 | </Card> |
| 295 | <Card className="p-4"> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 296 | <p className="text-sm font-medium">Time per run, in minutes</p> |
| 297 | <p className="mt-1 text-sm text-muted">Empty: as {parent}, shown faded.</p> | |
| 298 | <div className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3"> | |
| 299 | {RUN_KINDS.map((kind) => ( | |
| 300 | <label key={kind} className="flex flex-col gap-1 text-xs text-muted"> | |
| 301 | {RUN_KIND_LABEL[kind]} | |
| 302 | <Input | |
| 303 | name={`minutes:${kind}`} | |
| 304 | inputMode="numeric" | |
| 305 | defaultValue={own.minutes?.[kind] == null ? "" : String(own.minutes[kind])} | |
| 306 | placeholder={String(base.minutes[kind] ?? "")} | |
| 307 | /> | |
| 308 | </label> | |
| 309 | ))} | |
| 310 | </div> | |
| 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. | 311 | </Card> |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 312 | </Section> |
| 313 | </fieldset> | |
| 314 | ||
| 315 | {editable ? ( | |
| Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar | 316 | <div className="sticky bottom-(--tabbar-h) in-data-[keyboard=open]:bottom-0 -mx-4 flex flex-wrap items-center gap-4 border-t border-line bg-bg/90 px-4 py-4 backdrop-blur"> |
| Merge branch 'worktree-agent-a985c01f43538af01' | 317 | <SubmitButton pending="Saving…" busy={saving}> |
| 318 | Save guardrails | |
| 319 | </SubmitButton> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 320 | {saved && <span className="text-sm text-muted">Saved. Runs that start from now on get these.</span>} |
| 321 | <ErrorText>{error}</ErrorText> | |
| 322 | {own.updatedBy && own.updatedAt && !saved && !error && ( | |
| 323 | <span className="text-xs text-faint"> | |
| 324 | Last changed by <span className="font-mono">{own.updatedBy}</span> <TimeAgo at={own.updatedAt} /> | |
| 325 | </span> | |
| 326 | )} | |
| 327 | </div> | |
| 328 | ) : ( | |
| 329 | <p className="text-sm text-muted"> | |
| 330 | {level === "workspace" | |
| 331 | ? "Only the workspace's owners can change its defaults." | |
| 332 | : "Only members of the workspace can change a project's guardrails."} | |
| 333 | </p> | |
| 334 | )} | |
| 335 | </Form> | |
| 336 | ); | |
| 337 | } | |
| 338 | ||
| 339 | /** Minutes since `from`, ticking while the run goes on. */ | |
| 340 | function useMinutes(from: string | null, to: string | null): number | null { | |
| 341 | const [now, setNow] = useState(() => Date.now()); | |
| 342 | useEffect(() => { | |
| 343 | if (!from || to) return; | |
| 344 | const timer = setInterval(() => setNow(Date.now()), 5000); | |
| 345 | return () => clearInterval(timer); | |
| 346 | }, [from, to]); | |
| 347 | if (!from) return null; | |
| 348 | return ((to ? new Date(to).getTime() : now) - new Date(from).getTime()) / 60000; | |
| 349 | } | |
| 350 | ||
| 351 | function Meter({ share, tone }: { share: number | null; tone: string }) { | |
| 352 | if (share == null) return null; | |
| 353 | return ( | |
| 354 | <span className="mt-2 block h-1 overflow-hidden rounded-full bg-line" aria-hidden> | |
| 355 | <span className={`block h-full rounded-full ${tone}`} style={{ width: `${Math.round(share * 100)}%` }} /> | |
| 356 | </span> | |
| 357 | ); | |
| 358 | } | |
| 359 | ||
| 360 | /** A run's caps: spent and elapsed against them, and whether one stopped it. */ | |
| 361 | export function RunCaps({ run, member }: { run: AgentRun; member: boolean }) { | |
| 362 | const active = isActiveRun(run.status); | |
| 363 | const minutes = useMinutes(run.startedAt ?? (active ? run.createdAt : null), run.finishedAt); | |
| 364 | if (run.timeCapMinutes == null && run.budgetUsd == null && !run.halted) return null; | |
| 365 | const cap = run.timeCapMinutes ?? null; | |
| 366 | const timeShare = cap && minutes != null ? Math.min(1, minutes / cap) : null; | |
| 367 | const spent = formatCost(run.costUsd); | |
| 368 | const budget = run.budgetUsd ?? null; | |
| 369 | const costShare = budget && run.costUsd != null ? Math.min(1, run.costUsd / budget) : null; | |
| 370 | 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. | 371 | <Card asChild className="mt-6 p-4"> |
| 372 | <section> | |
| 373 | <div className="flex items-center gap-2 text-sm font-medium"> | |
| 374 | <ShieldCheck size={15} className="text-accent" /> | |
| 375 | Guardrails | |
| 376 | {run.halted && ( | |
| 377 | <Badge tone="warn" size="md" className="ml-auto"> | |
| 378 | {run.halted === "abuse" | |
| 379 | ? "Stopped: unusual CPU use" | |
| 380 | : `Stopped at its ${run.halted === "budget" ? "cost" : "time"} cap`} | |
| 381 | </Badge> | |
| 382 | )} | |
| 383 | </div> | |
| 384 | <div className="mt-3 grid gap-4 text-sm sm:grid-cols-2"> | |
| 385 | {cap != null && ( | |
| 386 | <div> | |
| 387 | <p className="flex items-center gap-1.5 text-muted"> | |
| 388 | <Clock size={13} /> | |
| 389 | <span className="tabular-nums text-fg" suppressHydrationWarning> | |
| 390 | {minutes == null ? "—" : `${Math.floor(minutes)}m`} | |
| 391 | </span> | |
| 392 | of {cap}m | |
| 393 | </p> | |
| 394 | <Meter share={timeShare} tone={timeShare != null && timeShare > 0.85 ? "bg-warn" : "bg-success"} /> | |
| 395 | </div> | |
| 396 | )} | |
| 397 | {member && ( | |
| 398 | <div> | |
| 399 | <p className="flex items-center gap-1.5 text-muted"> | |
| 400 | <Coins size={13} /> | |
| 401 | {spent ? <span className="tabular-nums text-fg">{spent}</span> : <span>Spend</span>} | |
| 402 | of {formatCap(budget)} | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 403 | </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. | 404 | <Meter share={costShare} tone={costShare != null && costShare > 0.85 ? "bg-warn" : "bg-success"} /> |
| 405 | {active && !spent && budget != null && ( | |
| 406 | <p className="mt-1.5 text-xs text-faint"> | |
| 407 | The agent stops itself at the cap. What it spent is reported when the run ends. | |
| 408 | </p> | |
| 409 | )} | |
| 410 | </div> | |
| 411 | )} | |
| 412 | </div> | |
| 413 | </section> | |
| 414 | </Card> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 415 | ); |
| 416 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.