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/services/runner/src/credentials.ts

96 lines4,154 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1/**
2 * Run credentials: the tokens a sandbox works with. Each is bound to its
3 * run, its repository and what that kind of run needs, acts as an agent on
4 * behalf of the person who started the work, and is revoked the moment
5 * the sandbox stops. See `crates/contracts/src/credentials.rs` for what
6 * each kind of run may do.
7 */
8
9import type { CreateRunCredentialInput, GitGrant, RepoPath, ServiceBinding } from "@g1t/contracts";
10
11/** The environment variables a sandbox's g1t tokens are passed in. */
12export const CREDENTIAL_VARS = ["G1T_TOKEN", "G1T_AGENT_TOKEN"] as const;
13
14const STORAGE_KEY = "credentials";
15
16/** Identity's JSON protocol, as the contracts' client speaks it. */
17async function call<T>(identity: ServiceBinding, method: string, args: object): Promise<T> {
18 const response = await identity.fetch(`https://service/rpc/${method}`, {
19 method: "POST",
20 headers: { "content-type": "application/json" },
21 body: JSON.stringify(args),
22 });
23 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
24 return (await response.json()) as T;
25}
26
27/** A run credential's text. */
28export async function runCredential(identity: ServiceBinding, input: CreateRunCredentialInput): Promise<string> {
29 const { token } = await call<{ token: string }>(identity, "create_run_credential", input);
30 return token;
31}
32
33/** The repository a `https://g1t.sh/<namespace>/<name>.git` remote names. */
34export function remotePath(url: string): RepoPath | null {
35 const match = /^https:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(url);
36 return match ? { namespace: match[1], name: match[2] } : null;
37}
38
39function samePath(a: RepoPath, b: RepoPath): boolean {
40 return a.namespace.toLowerCase() === b.namespace.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
41}
42
43/**
44 * Where a run working on a pull request may push: anywhere in the pull
45 * request's fork, which is its own; only its branch when the change is a
46 * branch of the repository itself.
47 */
48export function pushGrant(repo: RepoPath, source: RepoPath, branch: string | null | undefined): GitGrant {
49 return samePath(repo, source) ? { repo: source, branch: branch ?? null } : { repo: source, branch: null };
50}
51
52/** SHA-256 in lowercase hex, as identity stores tokens. */
53export async function sha256Hex(text: string): Promise<string> {
54 const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
55 return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
56}
57
58/** The hashes of the g1t tokens among a sandbox's variables. */
59export async function credentialHashes(envVars: Record<string, string>): Promise<string[]> {
60 const tokens = CREDENTIAL_VARS.map((name) => envVars[name]).filter(
61 (value): value is string => typeof value === "string" && value.startsWith("g1t_"),
62 );
63 return Promise.all(tokens.map(sha256Hex));
64}
65
66type Storage = {
67 put(key: string, value: unknown): Promise<void>;
68 get<T>(key: string): Promise<T | undefined>;
69 delete(key: string): Promise<boolean>;
70};
71
72/**
73 * Remembers a sandbox's credentials, by hash, so they can be revoked when
74 * it stops, and ties them to the run it recorded. Never stops the sandbox
75 * from starting.
76 */
77export async function holdCredentials(
78 identity: ServiceBinding,
79 storage: Storage,
80 envVars: Record<string, string>,
81 runId: string | null,
82): Promise<void> {
83 const hashes = await credentialHashes(envVars);
84 if (hashes.length === 0) return;
85 await storage.put(STORAGE_KEY, hashes);
86 if (!runId) return;
87 await call(identity, "bind_run_credentials", { tokenHashes: hashes, runId }).catch((error: unknown) => console.log("run credentials not bound", runId, String(error)));
88}
89
90/** Ends a sandbox's credentials, once. */
91export async function revokeCredentials(identity: ServiceBinding, storage: Storage): Promise<void> {
92 const hashes = await storage.get<string[]>(STORAGE_KEY);
93 if (!hashes?.length) return;
94 await storage.delete(STORAGE_KEY);
95 await call(identity, "revoke_run_credentials", { tokenHashes: hashes }).catch((error: unknown) => console.log("run credentials not revoked", String(error)));
96}