Skip to content

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.sh
133 linesCodeBlameRaw

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.

The runner base keeps its old comment, so the pushed base image still matches its folder1/**
2 * Who may read what in the context hub. A workspace is the boundary: no
3 * search ever reads another's rows. Inside it, someone who can read every
4 * repository (an owner, a member while the workspace's base permission is
5 * Read or more, and the workspace's own agents) reads everything; anyone
6 * else reads what comes from the projects whose repositories they can read:
7 * public ones, and private ones granted to them. Memory is for members.
8 * Pure, so the rules are tested apart from the index.
9 */
10
11import type { EntityKind, RepoPath, SearchHit, SearchKind } from "@g1t/contracts";
12
13export type Reader = {
14 workspace: string;
15 /** Belongs to the workspace: reads its memory. */
16 member: boolean;
17 /** Can read every repository in the workspace. */
18 full: boolean;
19 /** For someone who cannot read every repository: the slugs of the projects they may see. */
20 visible: Set<string>;
21 /** Whether any of `visible` is private: the index is then not narrowed to public rows. */
22 privateVisible?: boolean;
23 /** For someone who cannot read every repository: the `namespace/name` of those they can, lowercased. */
24 repos?: Set<string>;
25};
26
27/** The metadata every row of the search index carries. */
28export type IndexMeta = {
29 workspace: string;
30 kind: string;
31 project: string;
32 private: boolean;
33 title: string;
34 snippet: string;
35 url: string;
36 source: string;
37 by: string;
38 at: string;
39};
40
41/**
42 * The search index's filter for a reader: their workspace always; public
43 * rows only for someone who may read nothing private there (memory is
44 * private, so not for a member). `readable` checks each row again.
45 */
46export function indexFilter(
47 reader: Reader,
48 options: { project?: string | null; kinds?: SearchKind[] | null },
49): Record<string, unknown> {
50 const filter: Record<string, unknown> = { workspace: reader.workspace };
51 if (!reader.full && !reader.member && !reader.privateVisible) filter.private = false;
52 if (options.project) filter.project = options.project;
53 const kinds = allowedKinds(reader, options.kinds);
54 if (kinds) filter.kind = { $in: kinds };
55 return filter;
56}
57
58/** The kinds a reader may ask for: all of them, less memory for someone not a member. Null for all. */
59export function allowedKinds(reader: Reader, kinds?: SearchKind[] | null): SearchKind[] | null {
60 const wanted = kinds?.length ? kinds : null;
61 if (reader.member) return wanted;
62 const all: SearchKind[] = ["project", "app", "api", "package", "language", "owner", "environment", "integration", "doc", "issue", "pull"];
63 return (wanted ?? all).filter((kind) => kind !== "memory");
64}
65
66/** Whether a row from the index may be shown to a reader: checked again, whatever the filter did. */
67export function readable(meta: Pick<IndexMeta, "workspace" | "kind" | "project" | "private">, reader: Reader): boolean {
68 if (meta.workspace !== reader.workspace) return false;
69 if (reader.full) return true;
70 if (meta.kind === "memory") {
71 if (!reader.member) return false;
72 // Workspace memory is for every member; a project's, for those who can read it.
73 return !meta.project || reader.visible.has(meta.project);
74 }
75 // A private row needs its project readable: one with none is hidden.
76 if (meta.private) return !!meta.project && reader.visible.has(meta.project);
77 // A project made private since it was indexed is hidden at once.
78 return !meta.project || reader.visible.has(meta.project);
79}
80
81/** Whether a memory may be shown to a reader: a member's, and a project's only to those who can read its repository. */
82export function memoryReadable(repo: RepoPath | null | undefined, reader: Reader): boolean {
83 if (!reader.member) return false;
84 if (reader.full || !repo) return true;
85 return !!reader.repos?.has(`${repo.namespace}/${repo.name}`.toLowerCase());
86}
87
88/**
89 * Whether a memory may go into the prompt of an agent run acting for
90 * someone. `reader` is theirs; null is the workspace's own step, which is
91 * told everything. A member is told what they could read themselves; anyone
92 * else (an outside collaborator) only the memory of the repository the run
93 * is in, `repo` (`namespace/name`), never the workspace's.
94 */
95export function runMemoryReadable(
96 memory: { scope?: string | null; repo?: RepoPath | null },
97 reader: Reader | null,
98 repo: string,
99): boolean {
100 if (!reader) return true;
101 if (reader.member) return memoryReadable(memory.repo, reader);
102 return memory.scope !== "workspace" && !!memory.repo && `${memory.repo.namespace}/${memory.repo.name}`.toLowerCase() === repo.toLowerCase();
103}
104
105/**
106 * Entity counts by kind over the rows a reader may see, from rows grouped by
107 * kind, project and privacy.
108 */
109export function countVisible<K extends string>(
110 rows: { kind: K; project: string | null; private: number; n: number }[],
111 reader: Reader,
112): Partial<Record<K, number>> {
113 const counts: Partial<Record<K, number>> = {};
114 for (const row of rows) {
115 if (!readable({ workspace: reader.workspace, kind: "entity", project: row.project ?? "", private: !!row.private }, reader)) continue;
116 counts[row.kind] = (counts[row.kind] ?? 0) + row.n;
117 }
118 return counts;
119}
120
121/** Semantic hits first, then text matches not already among them, at most `limit`. */
122export function merge(semantic: SearchHit[], text: SearchHit[], limit: number): SearchHit[] {
123 const seen = new Set<string>();
124 const out: SearchHit[] = [];
125 for (const hit of [...semantic.sort((a, b) => b.score - a.score), ...text]) {
126 const key = `${hit.kind}:${hit.id}`;
127 if (seen.has(key)) continue;
128 seen.add(key);
129 out.push(hit);
130 if (out.length >= limit) break;
131 }
132 return out;
133}

This file's history is long; its oldest lines are credited to the oldest commit read.