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/packages/contracts/src/audit.ts

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