Skip to content

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.sh
166 linesCodeBlameRaw
1/**
2 * Run credentials and the audit log. Mirrors `crates/contracts/src/credentials.rs`
3 * and `crates/contracts/src/audit.rs`.
4 *
5 * Every sandbox run gets its own tokens, bound to the run, its repository
6 * and what its kind of work needs, and acting as an agent on behalf of the
7 * person who started the work. Everything done with them is recorded in
8 * the workspace's audit log, with the rule that allowed or refused it.
9 */
10
11import type { ServiceBinding } from "./clients";
12import type { RepoPath } from "./repos";
13import type { User } from "./identity";
14
15/** What a run does, as far as its credentials are concerned. */
16export type RunCredentialKind =
17 | "implement"
18 | "revise"
19 | "review"
20 | "answer"
21 | "update"
22 | "plan"
23 | "checks"
24 | "queue"
25 | "mergecheck"
26 | "deploy"
27 | "bump"
28 /** A session g1t started on an agent's harness: g1t acting for the person, in one repository. */
29 | "session";
30
31/**
32 * `runner`: g1t's runner in the sandbox, which clones, pushes and records
33 * the session, acting downstream as the person (a session's, also over the
34 * API, MCP and the CLI, as g1t for them). `tools`: the agent's own MCP
35 * tools, acting as the agent.
36 */
37export type CredentialUse = "runner" | "tools";
38
39/** A repository a run may push to; `branch` null means any branch of it. */
40export type GitGrant = { repo: RepoPath; branch?: string | null };
41
42/** What binds a run's token to one run. */
43export type RunBinding = {
44 kind: RunCredentialKind;
45 use: CredentialUse;
46 runId?: string | null;
47 number?: number | null;
48 agent: string;
49 read: RepoPath[];
50 push: GitGrant[];
51 /** g1t's own run: the workspace's credential, acting on behalf of g1t. */
52 system?: boolean;
53};
54
55/** Set on a run's token (or a historical `@g1t` agent's) resolved from its token: the composite identity. */
56export type Acting = {
57 credentialId: string;
58 agent: string;
59 onBehalfOf: { id: string; username: string };
60 scope: { repo: RepoPath; operations: string[]; run?: RunBinding };
61};
62
63export type CreateRunCredentialInput = {
64 onBehalfOf: User;
65 repo: RepoPath;
66 kind: RunCredentialKind;
67 use: CredentialUse;
68 /** The pull request the run works on. */
69 number?: number | null;
70 /** Repositories it may clone besides those it may push to. */
71 read?: RepoPath[];
72 push?: GitGrant[];
73 /** The run's timeout. */
74 ttlSeconds: number;
75};
76
77export type ActorKind = "person" | "agent" | "workspace" | "runner" | "system";
78export type AuditOutcome = "allowed" | "denied";
79/** Where it came in: the API, MCP, git, or g1t.sh's own pages. */
80export type AuditSurface = "rest" | "mcp" | "git" | "web" | "registry";
81
82export type AuditEntry = {
83 id: string;
84 /** RFC 3339. */
85 time: string;
86 actorKind: ActorKind;
87 /** A person's username, the agent's name, or a workspace's slug. */
88 actor: string;
89 actorId: string;
90 agent: string | null;
91 /** The person an agent acted for. */
92 onBehalfOf: string | null;
93 runId: string | null;
94 runKind: string | null;
95 credentialId: string | null;
96 /** An operation such as `create_issue`, or `git.push` and `git.fetch`. */
97 action: string;
98 surface: AuditSurface;
99 workspace: string;
100 /** `owner/name`. */
101 repo: string | null;
102 number: number | null;
103 gitRef: string | null;
104 path: string | null;
105 outcome: AuditOutcome;
106 /** The rule that allowed or refused it, such as `run:implement/tools` or `scope:repository`. */
107 rule: string;
108 /** `ok`, or the failure's code. */
109 result: string | null;
110 message: string | null;
111 requestId: string;
112};
113
114/**
115 * Which of a workspace's entries the viewer may see: an owner everything;
116 * a member what was done to its projects, and what they did or had done
117 * on their behalf.
118 */
119export type AuditVisibility = { kind: "all" } | { kind: "projects"; username: string };
120
121export type AuditQuery = {
122 workspace: string;
123 visibility: AuditVisibility;
124 actor?: string | null;
125 agent?: string | null;
126 action?: string | null;
127 repo?: string | null;
128 number?: number | null;
129 outcome?: AuditOutcome | null;
130 actorKind?: ActorKind | null;
131 runIds?: string[];
132 /** RFC 3339, inclusive. */
133 since?: string | null;
134 /** RFC 3339, exclusive. */
135 until?: string | null;
136 before?: string | null;
137 /** At most 500. */
138 limit?: number | null;
139};
140
141export type AuditPage = { entries: AuditEntry[]; next: string | null };
142
143export interface AuditApi {
144 /** Newest first. The caller has checked who may see the workspace's log. */
145 list(query: AuditQuery): Promise<AuditPage>;
146}
147
148/** The audit log, which the events service keeps. */
149export function auditClient(events: ServiceBinding): AuditApi {
150 return {
151 list: async (query) => {
152 const response = await events.fetch("https://service/rpc/audit_list", {
153 method: "POST",
154 headers: { "content-type": "application/json" },
155 body: JSON.stringify(query),
156 });
157 if (!response.ok) throw new Error(`audit_list failed with status ${response.status}`);
158 return (await response.json()) as AuditPage;
159 },
160 };
161}
162
163/** How an actor is shown: "g1t on behalf of syntaqx". */
164export function describeActor(entry: Pick<AuditEntry, "actor" | "agent" | "onBehalfOf">): string {
165 return entry.onBehalfOf ? `${entry.agent ?? entry.actor} on behalf of ${entry.onBehalfOf}` : entry.actor;
166}