g1t/packages/contracts/src/audit.ts
| 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 | |
| 11 | import type { ServiceBinding } from "./clients"; |
| 12 | import type { RepoPath } from "./repos"; |
| 13 | import type { User } from "./identity"; |
| 14 | |
| 15 | /** What a run does, as far as its credentials are concerned. */ |
| 16 | export 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 | */ |
| 33 | export type CredentialUse = "runner" | "tools"; |
| 34 | |
| 35 | /** A repository a run may push to; `branch` null means any branch of it. */ |
| 36 | export type GitGrant = { repo: RepoPath; branch?: string | null }; |
| 37 | |
| 38 | /** What binds an agent's token to one run. */ |
| 39 | export 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. */ |
| 50 | export type Acting = { |
| 51 | credentialId: string; |
| 52 | agent: string; |
| 53 | onBehalfOf: { id: string; username: string }; |
| 54 | scope: { repo: RepoPath; operations: string[]; run?: RunBinding }; |
| 55 | }; |
| 56 | |
| 57 | export 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 | |
| 73 | export type ActorKind = "person" | "agent" | "workspace"; |
| 74 | export type AuditOutcome = "allowed" | "denied"; |
| 75 | export type AuditSurface = "rest" | "mcp" | "git"; |
| 76 | |
| 77 | export 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 | */ |
| 114 | export type AuditVisibility = { kind: "all" } | { kind: "projects"; username: string }; |
| 115 | |
| 116 | export 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 | |
| 136 | export type AuditPage = { entries: AuditEntry[]; next: string | null }; |
| 137 | |
| 138 | export 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. */ |
| 144 | export 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". */ |
| 159 | export 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 | } |