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 docs service answers every artifacts call: docs can be made, listed, shared, moved, trashed, restored, searched, versioned and edited live in their own rooms, agents read, write and recall them only where their person and everyone in the conversation can, and folio events go out on the bus, while Docs' pages keep working as before. | 1 | /** |
| 2 | * What an agent may reach in folios for the person it acts for (its | |
| 3 | * asker), and who else will see its answer (the audience): the leak rules | |
| The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were. | 4 | * (docs.g1t.sh/guides/agent-access/), apart from where rows come from. |
| The docs service answers every artifacts call: docs can be made, listed, shared, moved, trashed, restored, searched, versioned and edited live in their own rooms, agents read, write and recall them only where their person and everyone in the conversation can, and folio events go out on the bus, while Docs' pages keep working as before. | 5 | * Pure. |
| 6 | * | |
| 7 | * - The agent never has more than its asker (`agentFolioRole`). | |
| 8 | * - Finding things (lists, search, recall, stale) is narrowed to folios | |
| 9 | * every person in the audience can read, so nothing turns up in a | |
| 10 | * conversation that someone in it can't open. | |
| 11 | * - A public channel's audience is "the workspace": only folios readable | |
| 12 | * through an open space or general access `workspace`. Never a link | |
| 13 | * folio, a share, or Private. More than 20 people reads the same way. | |
| 14 | * - Reading one folio the asker named (a link they gave) works whenever | |
| 15 | * the asker can read it; the result says `audience_can_read: false` | |
| 16 | * when someone else in the conversation can't, so the agent says it | |
| 17 | * sent the link to the asker instead of quoting it. | |
| 18 | */ | |
| 19 | import type { DocAgentAbilities, DocAgentMode, DocAudience, DocRole } from "@g1t/contracts"; | |
| 20 | ||
| 21 | import { agentAbilities, agentFolioRole, effectiveRole, folioReadableByWorkspace, roleOf, type FolioAclNode, type FolioGrants, type Person, type SpaceRules } from "../access.ts"; | |
| 22 | ||
| 23 | /** More people than this in a conversation read as the whole workspace. */ | |
| 24 | export const AUDIENCE_AS_WORKSPACE = 20; | |
| 25 | ||
| 26 | /** Who an answer reaches, as access checks it. */ | |
| 27 | export type AudienceRule = | |
| 28 | /** The asker alone (no audience, or only them). */ | |
| 29 | | { kind: "asker" } | |
| 30 | /** These other people besides the asker, by user id. */ | |
| 31 | | { kind: "people"; user_ids: string[] } | |
| 32 | /** Everyone in the workspace. */ | |
| 33 | | { kind: "workspace" }; | |
| 34 | ||
| 35 | export function audienceRule(audience: DocAudience | null | undefined, askerId: string): AudienceRule { | |
| 36 | if (!audience) return { kind: "asker" }; | |
| 37 | if (audience.kind === "workspace") return { kind: "workspace" }; | |
| 38 | if (audience.kind !== "people" || !Array.isArray(audience.user_ids)) return { kind: "asker" }; | |
| 39 | const others = [...new Set(audience.user_ids.map(String))].filter((id) => id && id !== askerId); | |
| 40 | if (!others.length) return { kind: "asker" }; | |
| 41 | if (others.length + 1 > AUDIENCE_AS_WORKSPACE) return { kind: "workspace" }; | |
| 42 | return { kind: "people", user_ids: others }; | |
| 43 | } | |
| 44 | ||
| 45 | /** One folio as an agent sees it. */ | |
| 46 | export type AgentReach = { | |
| 47 | /** The asker's role, or null when they can't read it. */ | |
| 48 | asker_role: DocRole | null; | |
| 49 | /** Whether everyone in the audience can read it too. */ | |
| 50 | audience_can_read: boolean; | |
| 51 | /** What the agent may do: the asker's role, nothing more, by the folio's agent mode. */ | |
| 52 | can: DocAgentAbilities; | |
| 53 | }; | |
| 54 | ||
| 55 | export type ReachInput = { | |
| 56 | chain: readonly FolioAclNode[]; | |
| 57 | grants: FolioGrants; | |
| 58 | /** The folio's space's rules, when its chain inherits one (else null). */ | |
| 59 | space: SpaceRules | null; | |
| 60 | asker: Person; | |
| 61 | askerVisited: boolean; | |
| 62 | rule: AudienceRule; | |
| 63 | /** The audience's people (for a `people` rule). */ | |
| 64 | people: readonly Person[]; | |
| 65 | /** Whether a person opened the folio's link. */ | |
| 66 | visited: (person: Person) => boolean; | |
| 67 | agent_mode: DocAgentMode; | |
| 68 | }; | |
| 69 | ||
| 70 | export function agentReach(input: ReachInput): AgentReach { | |
| 71 | const spaceRole = (p: Person) => (input.space ? roleOf(input.space, p) : null); | |
| 72 | const asker = effectiveRole(input.chain, input.grants, spaceRole(input.asker), input.asker, { visited: input.askerVisited }); | |
| 73 | let audience = true; | |
| 74 | if (input.rule.kind === "workspace") audience = folioReadableByWorkspace(input.chain, input.space); | |
| 75 | else if (input.rule.kind === "people") audience = input.people.every((p) => !!effectiveRole(input.chain, input.grants, spaceRole(p), p, { visited: input.visited(p) })); | |
| 76 | const role = agentFolioRole(asker, true); | |
| 77 | return { asker_role: role, audience_can_read: !!asker && audience, can: agentAbilities(role, input.agent_mode) }; | |
| 78 | } | |
| 79 | ||
| 80 | /** Whether an agent may find a folio (lists, search, recall): its asker and every person in the audience can read it. */ | |
| 81 | export function agentMayFind(reach: AgentReach): boolean { | |
| 82 | return !!reach.asker_role && reach.audience_can_read; | |
| 83 | } |