| 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 |
| 4 | * (docs.g1t.sh/guides/agent-access/), apart from where rows come from. |
| 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 | } |