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/context/src/visibility.ts

138 lines5,893 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 * Who may read what in the context hub. A workspace is the boundary: no
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look3 * 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.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API9 */
10
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11import type { EntityKind, RepoPath, SearchHit, SearchKind } from "@g1t/contracts";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API12
13export type Reader = {
14 workspace: string;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15 /** Belongs to the workspace: reads its memory. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API16 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look17 /** 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. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API20 visible: Set<string>;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21 /** 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>;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API25};
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
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look41/**
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 */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API46export 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 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51 if (!reader.full && !reader.member && !reader.privateVisible) filter.private = false;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API52 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;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69 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);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API77 // A project made private since it was indexed is hidden at once.
78 return !meta.project || reader.visible.has(meta.project);
79}
80
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look81/** 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/** Whether a run acting for `reader` may be told of the project `slug` (a dependency, or one depending on its own). */
106export function projectReadable(slug: string, reader: Reader | null): boolean {
107 return !reader || reader.full || reader.visible.has(slug);
108}
109
110/**
111 * Entity counts by kind over the rows a reader may see, from rows grouped by
112 * kind, project and privacy.
113 */
114export function countVisible<K extends string>(
115 rows: { kind: K; project: string | null; private: number; n: number }[],
116 reader: Reader,
117): Partial<Record<K, number>> {
118 const counts: Partial<Record<K, number>> = {};
119 for (const row of rows) {
120 if (!readable({ workspace: reader.workspace, kind: "entity", project: row.project ?? "", private: !!row.private }, reader)) continue;
121 counts[row.kind] = (counts[row.kind] ?? 0) + row.n;
122 }
123 return counts;
124}
125
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API126/** Semantic hits first, then text matches not already among them, at most `limit`. */
127export function merge(semantic: SearchHit[], text: SearchHit[], limit: number): SearchHit[] {
128 const seen = new Set<string>();
129 const out: SearchHit[] = [];
130 for (const hit of [...semantic.sort((a, b) => b.score - a.score), ...text]) {
131 const key = `${hit.kind}:${hit.id}`;
132 if (seen.has(key)) continue;
133 seen.add(key);
134 out.push(hit);
135 if (out.length >= limit) break;
136 }
137 return out;
138}