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.
| Chat and workspace agents: channels, DMs and named agents you talk to | 1 | /** |
| 2 | * Who may see and who may change a workspace's agents. Pure, so it is | |
| 3 | * tested on its own. | |
| 4 | * | |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 5 | * Any member sees the workspace's agents: agents are members too, and |
| 6 | * people need to know who they can talk to. Only owners create, change or | |
| 7 | * archive them, since such an agent spends the workspace's money and acts | |
| 8 | * in its name. A workspace's own access token, acting as the workspace, | |
| 9 | * counts as an owner, as it does for the workspace's other settings. | |
| 10 | * | |
| 11 | * Personal agents (docs.g1t.sh/guides/agents/, "Personal agents") are a | |
| 12 | * member's own: any member may create one while the workspace lets members | |
| 13 | * (`members_create_agents`, on unless an owner turns it off). Only its | |
| 14 | * member sees it, changes it and talks to it, in their direct message with | |
| 15 | * it; owners see it too, archive it, and promote it to a workspace agent. | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 16 | */ |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 17 | import type { WorkspaceAgentScope, User } from "@g1t/contracts"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 18 | |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 19 | type Viewer = Pick<User, "kind" | "username" | "workspaces" | "verified"> & { id?: string } | null | undefined; |
| 20 | ||
| 21 | /** An agent as these rules read it. */ | |
| 22 | export type Owned = { scope?: string | null; owner_id?: string | null; builtin?: number | boolean }; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 23 | |
| 24 | export function canSee(viewer: Viewer, workspace: string): boolean { | |
| 25 | const slug = workspace.toLowerCase(); | |
| 26 | if (!viewer) return false; | |
| 27 | if (viewer.kind === "workspace") return viewer.username.toLowerCase() === slug; | |
| 28 | return !!viewer.workspaces?.some((m) => m.slug.toLowerCase() === slug); | |
| 29 | } | |
| 30 | ||
| 31 | export function canManage(viewer: Viewer, workspace: string): boolean { | |
| 32 | const slug = workspace.toLowerCase(); | |
| 33 | if (!viewer) return false; | |
| 34 | if (viewer.kind === "workspace") return viewer.username.toLowerCase() === slug; | |
| 35 | return !!viewer.workspaces?.some((m) => m.slug.toLowerCase() === slug && m.role === "owner"); | |
| 36 | } | |
| 37 | ||
| 38 | export const MANAGE_REFUSAL = "Only the workspace's owners can create, change or archive its agents."; | |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 39 | export const PERSONAL_OFF_REFUSAL = "This workspace's owners have turned off personal agents. Ask an owner to make the agent for the workspace."; |
| 40 | export const NOT_YOURS_REFUSAL = "Only the person whose personal agent this is can change it."; | |
| 41 | ||
| 42 | const personal = (agent: Owned) => agent.scope === "personal"; | |
| 43 | ||
| 44 | /** Whether `viewer` is the member a personal agent belongs to. */ | |
| 45 | export function isOwnerOf(viewer: Viewer, agent: Owned): boolean { | |
| 46 | return !!viewer && (viewer.kind ?? "user") === "user" && personal(agent) && !!agent.owner_id && viewer.id === agent.owner_id; | |
| 47 | } | |
| 48 | ||
| 49 | /** Whether the viewer sees this agent at all: a personal one only its member and the owners. */ | |
| 50 | export function canSeeAgent(viewer: Viewer, workspace: string, agent: Owned): boolean { | |
| 51 | if (!canSee(viewer, workspace)) return false; | |
| 52 | return !personal(agent) || isOwnerOf(viewer, agent) || canManage(viewer, workspace); | |
| 53 | } | |
| 54 | ||
| 55 | /** Whether the viewer may change the agent's definition: owners a workspace agent, its member a personal one. */ | |
| 56 | export function canChange(viewer: Viewer, workspace: string, agent: Owned): boolean { | |
| 57 | return personal(agent) ? isOwnerOf(viewer, agent) : canManage(viewer, workspace); | |
| 58 | } | |
| 59 | ||
| 60 | /** Whether the viewer may archive it: owners any agent, a member their own personal one. */ | |
| 61 | export function canArchive(viewer: Viewer, workspace: string, agent: Owned): boolean { | |
| 62 | return canManage(viewer, workspace) || isOwnerOf(viewer, agent); | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * The scope a new agent gets, or why the viewer may not create one: | |
| 67 | * owners make workspace agents unless they ask for a personal one; anyone | |
| 68 | * else makes personal agents, while the workspace lets them. | |
| 69 | */ | |
| 70 | export function creatableScope( | |
| 71 | viewer: Viewer, | |
| 72 | workspace: string, | |
| 73 | asked: unknown, | |
| 74 | membersMayCreate: boolean, | |
| 75 | ): { ok: true; scope: WorkspaceAgentScope } | { ok: false; message: string } { | |
| 76 | if (!canSee(viewer, workspace)) return { ok: false, message: "There is no such workspace." }; | |
| 77 | const owner = canManage(viewer, workspace); | |
| 78 | const scope: WorkspaceAgentScope = asked === "personal" || asked === "workspace" ? asked : owner ? "workspace" : "personal"; | |
| 79 | if (scope === "workspace") return owner ? { ok: true, scope } : { ok: false, message: MANAGE_REFUSAL }; | |
| 80 | // A personal agent belongs to a person: never to a workspace's token or another agent. | |
| 81 | if ((viewer!.kind ?? "user") !== "user" || !viewer!.id) return { ok: false, message: "A personal agent belongs to a person: create it signed in as yourself." }; | |
| 82 | if (!owner && !membersMayCreate) return { ok: false, message: PERSONAL_OFF_REFUSAL }; | |
| 83 | return { ok: true, scope }; | |
| 84 | } | |
| 85 | ||
| 86 | /** | |
| 87 | * Whether a personal agent may answer here: only its member, only in the | |
| 88 | * direct message of the two of them. `members` is how many people and | |
| 89 | * agents are in the conversation, when known. Null: it may; otherwise why not. | |
| 90 | */ | |
| 91 | export function personalRefusal(agent: Owned, place: { asked_by: string; channel_kind: "channel" | "dm"; members?: number | null }): string | null { | |
| 92 | if (!personal(agent)) return null; | |
| 93 | if (place.asked_by !== agent.owner_id) return "I'm someone's personal agent, so only they can talk to me."; | |
| 94 | if (place.channel_kind !== "dm" || (place.members != null && place.members > 2)) { | |
| 95 | return "I'm a personal agent: I answer only in my direct message with the person I belong to."; | |
| 96 | } | |
| 97 | return null; | |
| 98 | } |