Skip to content
83 linesCodeBlameRaw

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
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}