Skip to content
83 linesCodeBlameRaw
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 * of docs/ARTIFACTS_MODE.md section 4.3, 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 */
19import type { DocAgentAbilities, DocAgentMode, DocAudience, DocRole } from "@g1t/contracts";
20
21import { 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. */
24export const AUDIENCE_AS_WORKSPACE = 20;
25
26/** Who an answer reaches, as access checks it. */
27export 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
35export 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. */
46export 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
55export 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
70export 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. */
81export function agentMayFind(reach: AgentReach): boolean {
82 return !!reach.asker_role && reach.audience_can_read;
83}