g1t/packages/contracts/src/audit.ts

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