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

161 lines4,915 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";
75export type AuditSurface = "rest" | "mcp" | "git";
76
77export type AuditEntry = {
78 id: string;
79 /** RFC 3339. */
80 time: string;
81 actorKind: ActorKind;
82 /** A person's username, the agent's name, or a workspace's slug. */
83 actor: string;
84 actorId: string;
85 agent: string | null;
86 /** The person an agent acted for. */
87 onBehalfOf: string | null;
88 runId: string | null;
89 runKind: string | null;
90 credentialId: string | null;
91 /** An operation such as `create_issue`, or `git.push` and `git.fetch`. */
92 action: string;
93 surface: AuditSurface;
94 workspace: string;
95 /** `owner/name`. */
96 repo: string | null;
97 number: number | null;
98 gitRef: string | null;
99 path: string | null;
100 outcome: AuditOutcome;
101 /** The rule that allowed or refused it, such as `run:implement/tools` or `scope:repository`. */
102 rule: string;
103 /** `ok`, or the failure's code. */
104 result: string | null;
105 message: string | null;
106 requestId: string;
107};
108
109/**
110 * Which of a workspace's entries the viewer may see: an owner everything;
111 * a member what was done to its projects, and what they did or had done
112 * on their behalf.
113 */
114export type AuditVisibility = { kind: "all" } | { kind: "projects"; username: string };
115
116export type AuditQuery = {
117 workspace: string;
118 visibility: AuditVisibility;
119 actor?: string | null;
120 agent?: string | null;
121 action?: string | null;
122 repo?: string | null;
123 number?: number | null;
124 outcome?: AuditOutcome | null;
125 actorKind?: ActorKind | null;
126 runIds?: string[];
127 /** RFC 3339, inclusive. */
128 since?: string | null;
129 /** RFC 3339, exclusive. */
130 until?: string | null;
131 before?: string | null;
132 /** At most 500. */
133 limit?: number | null;
134};
135
136export type AuditPage = { entries: AuditEntry[]; next: string | null };
137
138export interface AuditApi {
139 /** Newest first. The caller has checked who may see the workspace's log. */
140 list(query: AuditQuery): Promise<AuditPage>;
141}
142
143/** The audit log, which the events service keeps. */
144export function auditClient(events: ServiceBinding): AuditApi {
145 return {
146 list: async (query) => {
147 const response = await events.fetch("https://service/rpc/audit_list", {
148 method: "POST",
149 headers: { "content-type": "application/json" },
150 body: JSON.stringify(query),
151 });
152 if (!response.ok) throw new Error(`audit_list failed with status ${response.status}`);
153 return (await response.json()) as AuditPage;
154 },
155 };
156}
157
158/** How an actor is shown: "g1t-agent on behalf of syntaqx". */
159export function describeActor(entry: Pick<AuditEntry, "actor" | "agent" | "onBehalfOf">): string {
160 return entry.onBehalfOf ? `${entry.agent ?? entry.actor} on behalf of ${entry.onBehalfOf}` : entry.actor;
161}