g1t/services/context/src/visibility.ts
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 API | 1 | /** |
| 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 look | 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. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 9 | */ |
| 10 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 11 | import 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 API | 12 | |
| 13 | export type Reader = { | |
| 14 | workspace: string; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 15 | /** 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 API | 16 | member: boolean; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 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. */ | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 20 | visible: Set<string>; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 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>; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 25 | }; |
| 26 | ||
| 27 | /** The metadata every row of the search index carries. */ | |
| 28 | export 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 look | 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 | */ | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 46 | export 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 look | 51 | 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 API | 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. */ | |
| 59 | export 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. */ | |
| 67 | export 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 look | 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); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 77 | // 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 look | 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. */ |
| 82 | export 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 | */ | |
| 95 | export 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). */ | |
| 106 | export 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 | */ | |
| 114 | export 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 API | 126 | /** Semantic hits first, then text matches not already among them, at most `limit`. */ |
| 127 | export 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 | } |