Skip to content
677 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.

Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.1/**
2 * An agent's abilities (docs.g1t.sh/guides/agent-abilities/): what it can
3 * do, in groups, and for each whether it does it on its own, only when the
4 * person asked for it, after asking first, or never.
5 *
6 * - **g1t's own** (artifacts, chat, code, issues and pull requests, files,
7 * colleagues) are always on, within the access of the person it acts
8 * for. The four that write code and docs keep the agent's `autonomy`
9 * choices, shown here as levels.
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.10 * - **Its computer**: a shell and files on a computer of its own, which
11 * it uses in sessions (docs.g1t.sh/guides/agents/, "Its computer"); a
12 * browser and web search are coming.
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.13 * - **Integrations**: every connector the workspace has connected lists
14 * what an agent can do through it, one row per action, from the
15 * connector catalog (./connectors.ts) and what the integrations service
16 * does today. Each row says whose connection it runs on: the
17 * workspace's, or the asking person's own.
18 * - **MCP servers** an owner adds: each tool the server lists is a row. A
19 * tool that doesn't say it only reads is treated as a write.
20 *
21 * Levels default by what a thing does: reading is alone; writing inside
22 * g1t is alone when the person asked for it; sending outside g1t asks
23 * first; purchases, credentials and permission changes are never, and
24 * can't be raised above asking.
25 *
26 * The agents service enforces these in code when it offers tools and when
27 * one is called; the Abilities tab and the agents service both resolve an
28 * agent's choices with `resolveAbilities`, handing it the connector catalog
29 * (./connectors.ts `CONNECTORS`): no value imports here, so services test
30 * it under Node as it is. Wire shapes are snake_case.
31 */
32import type { Connector } from "./connectors";
33import type { AgentAutonomy } from "./workspace-agents";
34
35/** Alone; alone when the asker asked for it; ask first; never. */
36export type AbilityLevel = "alone" | "asked" | "ask" | "never";
37
38export const ABILITY_LEVELS: readonly AbilityLevel[] = ["alone", "asked", "ask", "never"];
39
40export const ABILITY_LEVEL_LABELS: Record<AbilityLevel, string> = {
41 alone: "Alone",
42 asked: "Alone when asked for it",
43 ask: "Ask first",
44 never: "Never",
45};
46
47/** How much a level lets through: a lower number is freer. */
48const ORDER: Record<AbilityLevel, number> = { alone: 0, asked: 1, ask: 2, never: 3 };
49
50export type AbilityGroup = "g1t" | "computer" | "integration" | "mcp";
51
52/**
53 * What an ability does: `read`, `write` (inside g1t), `send` (something
54 * leaves g1t: a comment, a message, a change in another system) or
55 * `restricted` (purchases, credentials, permission changes).
56 */
57export type AbilityKind = "read" | "write" | "send" | "restricted";
58
59/** Whose connection an integration ability runs on. */
60export type AbilityCredentials = "workspace" | "asker";
61
62export type AbilityStatus = "ready" | "coming";
63
64/** The level a kind starts at. */
65export function defaultLevel(kind: AbilityKind): AbilityLevel {
66 switch (kind) {
67 case "read":
68 return "alone";
69 case "write":
70 return "asked";
71 case "send":
72 return "ask";
73 case "restricted":
74 return "never";
75 }
76}
77
78/** The freest level a kind may be set to: restricted things never go above asking. */
79export function maxLevel(kind: AbilityKind): AbilityLevel {
80 return kind === "restricted" ? "ask" : "alone";
81}
82
83/** Whether `level` is within `max`: no freer than it. */
84export function withinLevel(level: AbilityLevel, max: AbilityLevel): boolean {
85 return ORDER[level] >= ORDER[max];
86}
87
88/** One ability as the catalog defines it. */
89export type AbilityDef = {
90 /** `g1t:artifacts`, `integration:linear:read`, `mcp:<server>:<tool>`, `computer:shell`. */
91 id: string;
92 group: AbilityGroup;
93 label: string;
94 /** What it does, in a line. */
95 about: string;
96 kind: AbilityKind;
97 /** The agent tools it offers (the agents service's names), for the row's chips. */
98 tools: string[];
99 status: AbilityStatus;
100 /**
101 * The agent's `autonomy` field it reads and writes, for g1t's own code
102 * and docs abilities, with the levels that field allows.
103 */
104 autonomy?: { key: keyof AgentAutonomy; levels: AbilityLevel[] } | null;
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.105 /** The level it starts at, when that is not what its kind would say. */
106 default?: AbilityLevel | null;
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.107};
108
109/** What an agent keeps for one ability. Absent fields mean the default. */
110export type AbilitySetting = { level?: AbilityLevel | null; credentials?: AbilityCredentials | null };
111
112/** A tool an MCP server lists, as it was found. */
113export type McpTool = {
114 name: string;
115 description: string;
116 /** `read` when the server says the tool only reads; `write` otherwise, which is also what an unknown tool is. */
117 kind: "read" | "write";
118 /** Its arguments, as the server describes them (JSON Schema); what the agent is offered. */
119 input_schema: Record<string, unknown>;
120};
121
122/** An MCP server an owner added to one agent. */
123export type McpServer = {
124 /** `mcp_…`, stable: ability ids are made from it. */
125 id: string;
126 /** Lowercase letters, digits and hyphens: the tools are offered as `<name>__<tool>`. */
127 name: string;
128 /** Its HTTPS address; the host is checked when it is added. */
129 url: string;
130 tools: McpTool[];
131 added_by: string;
132 /** RFC 3339. */
133 added_at: string;
134 /** When its tools were last listed, and what went wrong if they couldn't be. */
135 checked_at: string | null;
136 problem: string | null;
137};
138
139/** What an agent's definition keeps about its abilities. */
140export type AgentAbilities = {
141 /** By ability id: only what differs from the default is kept. */
142 settings: Record<string, AbilitySetting>;
143 mcp_servers: McpServer[];
144};
145
146export const EMPTY_ABILITIES: AgentAbilities = { settings: {}, mcp_servers: [] };
147
148/** What a change to an agent sends for its abilities: the settings, whole. MCP servers have their own calls. */
149export type AgentAbilitiesChange = { settings: Record<string, AbilitySetting> };
150
151/** The most MCP servers one agent has, and the most tools one server lists. */
152export const MAX_MCP_SERVERS = 10;
153export const MAX_MCP_TOOLS = 40;
154
155/** An ability as one agent has it now. */
156export type Ability = AbilityDef & {
157 level: AbilityLevel;
158 /** The freest level it may be set to. */
159 max: AbilityLevel;
160 /** The levels it may be set to, freest first. */
161 choices: AbilityLevel[];
162 /** Whether the level can be changed at all: g1t's own reads are always on. */
163 can_change: boolean;
164 /** For an integration: whose connection it runs on. Null elsewhere. */
165 credentials: AbilityCredentials | null;
166 /** Whether the asking person could connect it themselves (the connector has a personal side that is available). */
167 personal_available: boolean;
168};
169
170/** One source of abilities: a connector, an MCP server, or g1t itself. */
171export type AbilitySource = {
172 id: string;
173 name: string;
174 /** For an integration or an MCP server: whether the workspace has it. */
175 connected: boolean;
176 /** Where it is connected or managed; null when there is nowhere. */
177 href: string | null;
178 /** A line about it when it has no abilities, or something is wrong with it. */
179 note: string | null;
180 abilities: Ability[];
181};
182
183export type AbilitySection = {
184 group: AbilityGroup;
185 title: string;
186 about: string;
187 sources: AbilitySource[];
188};
189
190/** What a level means for an agent, in its own words. */
191export function levelWords(level: AbilityLevel): string {
192 switch (level) {
193 case "alone":
194 return "on its own";
195 case "asked":
196 return "only when the person asked for that";
197 case "ask":
198 return "after asking first";
199 case "never":
200 return "never";
201 }
202}
203
204// g1t's own ---------------------------------------------------------------
205
206/** g1t's own abilities: always on, within the asker's access; the four autonomy ones keep their choices. */
207export const G1T_ABILITIES: AbilityDef[] = [
208 {
209 id: "g1t:artifacts",
210 group: "g1t",
211 label: "Artifacts",
212 about: "Search, read, write, edit and share the workspace's artifacts, where the person it acts for can.",
213 kind: "read",
214 tools: ["search_artifacts", "read_artifact", "list_spaces", "stale_artifacts", "create_artifact", "edit_artifact", "share_artifact"],
215 status: "ready",
216 },
217 {
218 id: "g1t:chat",
219 group: "g1t",
220 label: "Chat",
221 about: "Search messages and read threads everyone in the conversation can read, and the roster.",
222 kind: "read",
223 tools: ["search_messages", "read_thread", "workspace_roster"],
224 status: "ready",
225 },
226 {
227 id: "g1t:code",
228 group: "g1t",
229 label: "Code",
230 about: "Read files and search code in repositories everyone in the conversation can read.",
231 kind: "read",
232 tools: ["list_repositories", "search_code", "read_file"],
233 status: "ready",
234 },
235 {
236 id: "g1t:issues",
237 group: "g1t",
238 label: "Issues and pull requests",
239 about: "Read issues and pull requests, draft issues as cards, and comment or review on behalf of the person who asked.",
240 kind: "read",
241 tools: ["list_issues", "get_issue", "get_pull", "recent_activity", "draft_issue", "comment", "review_pull"],
242 status: "ready",
243 },
244 {
245 id: "g1t:pulls",
246 group: "g1t",
247 label: "Open pull requests",
248 about: "Open pull requests from its sessions. Rules, protected branches and required checks still apply.",
249 kind: "write",
250 tools: [],
251 status: "ready",
252 autonomy: { key: "open_pull_requests", levels: ["alone", "ask"] },
253 },
254 {
255 id: "g1t:merge",
256 group: "g1t",
257 label: "Merge",
258 about: "Merge pull requests that have what the branch requires.",
259 kind: "write",
260 tools: [],
261 status: "ready",
262 autonomy: { key: "merge", levels: ["alone", "ask", "never"] },
263 },
264 {
265 id: "g1t:deploy",
266 group: "g1t",
267 label: "Deploy to production",
268 about: "Deploy a project's production environment.",
269 kind: "restricted",
270 tools: [],
271 status: "ready",
272 autonomy: { key: "deploy_production", levels: ["ask", "never"] },
273 },
274 {
275 id: "g1t:docs",
276 group: "g1t",
277 label: "Edit docs",
278 about: "Change docs directly, or leave each change as a suggestion someone accepts.",
279 kind: "write",
280 tools: ["edit_artifact"],
281 status: "ready",
282 autonomy: { key: "edit_docs", levels: ["alone", "ask"] },
283 },
284 {
285 id: "g1t:files",
286 group: "g1t",
287 label: "Make files",
288 about: "Make PDFs, Word documents, spreadsheets, CSVs and Markdown files, kept with a doc the person can open.",
289 kind: "write",
290 tools: ["make_file"],
291 status: "ready",
292 },
293 {
294 id: "g1t:colleagues",
295 group: "g1t",
296 label: "Colleagues and memory",
297 about: "Ask a colleague, hand work off, bring one into a session, use its subagents, start sessions, and remember facts.",
298 kind: "write",
299 tools: ["ask_colleague", "hand_off", "bring_in", "use_subagent", "start_session", "post_update", "remember", "forget", "use_skill"],
300 status: "ready",
301 },
302];
303
304/** `autonomy` values as levels, and back. */
305export function levelOfAutonomy(value: string): AbilityLevel {
306 switch (value) {
307 case "alone":
308 return "alone";
309 case "never":
310 return "never";
311 default:
312 // `approval` and `suggest` both mean a person acts first.
313 return "ask";
314 }
315}
316
317export function autonomyOfLevel(key: keyof AgentAutonomy, level: AbilityLevel): string {
318 if (level === "alone") return "alone";
319 if (level === "never") return "never";
320 return key === "edit_docs" ? "suggest" : "approval";
321}
322
323// Its computer -----------------------------------------------------------
324
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.325/**
326 * The agent's own computer: a persistent home on g1t cloud that its
327 * sessions use and that sleeps when idle. Running commands is a write it
328 * does alone when asked for it; reading and writing its own files is alone,
329 * since nothing leaves the computer. Chat replies never use it.
330 */
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.331export const COMPUTER_ABILITIES: AbilityDef[] = [
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.332 { id: "computer:shell", group: "computer", label: "Shell", about: "Run commands on its own computer, with a persistent home, from its sessions.", kind: "write", tools: ["run_command"], status: "ready" },
333 { id: "computer:files", group: "computer", label: "Files", about: "Read and write files in its computer's home.", kind: "write", tools: ["computer_read_file", "computer_write_file"], status: "ready", default: "alone" },
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.334 { id: "computer:browser", group: "computer", label: "Browser", about: "Open sites in a browser of its own, signed in where you let it be.", kind: "send", tools: [], status: "coming" },
335 { id: "computer:web", group: "computer", label: "Web search", about: "Search and read the open web, as its team's web access allows.", kind: "read", tools: [], status: "coming" },
336];
337
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.338/** The tools the computer offers, by ability id: what a session offers once the ability is not Never. */
339export const COMPUTER_TOOLS: Record<string, string[]> = Object.fromEntries(COMPUTER_ABILITIES.filter((def) => def.status === "ready").map((def) => [def.id, def.tools]));
340
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.341// Integrations -----------------------------------------------------------
342
343/**
344 * What an agent can do through each connector today, as the integrations
345 * service does it: look an item up by its key or address, open an issue in
346 * g1t from it, comment on it there, and (Sentry) mark it resolved. A
347 * connector not named here has nothing an agent calls: Datadog and the
348 * alerts webhook open issues by themselves; GitHub imports and mirrors
349 * repositories; model providers run models.
350 */
351const INTEGRATION_ACTIONS: Record<string, { action: string; label: string; about: string; kind: AbilityKind; tool: string }[]> = {
352 linear: [
353 { action: "read", label: "Read issues", about: "Look up an issue by its key (ENG-42) or address: its title, status and description.", kind: "read", tool: "lookup_outside" },
354 { action: "import", label: "Import issues", about: "Open an issue in a repository here from a Linear issue, linked back to it.", kind: "write", tool: "import_outside" },
355 { action: "comment", label: "Comment", about: "Comment on an issue in Linear, as the workspace's connection, naming the person it acts for.", kind: "send", tool: "act_outside" },
356 ],
357 jira: [
358 { action: "read", label: "Read tickets", about: "Look up a ticket by its key (TECH-1234) or address: its summary, status and description.", kind: "read", tool: "lookup_outside" },
359 { action: "import", label: "Import tickets", about: "Open an issue in a repository here from a Jira ticket, linked back to it.", kind: "write", tool: "import_outside" },
360 { action: "comment", label: "Comment", about: "Comment on a ticket in Jira, as the workspace's connection, naming the person it acts for.", kind: "send", tool: "act_outside" },
361 ],
362 sentry: [
363 { action: "read", label: "Read issues", about: "Look up a Sentry issue by its address: its title, status and latest stack trace.", kind: "read", tool: "lookup_outside" },
364 { action: "import", label: "Import issues", about: "Open an issue in a repository here from a Sentry issue, linked back to it.", kind: "write", tool: "import_outside" },
365 { action: "comment", label: "Comment", about: "Leave a note on a Sentry issue, as the workspace's connection.", kind: "send", tool: "act_outside" },
366 { action: "resolve", label: "Resolve issues", about: "Mark a Sentry issue resolved, with a note.", kind: "send", tool: "act_outside" },
367 ],
368};
369
370/** What a connected connector with nothing for an agent to call says. */
371function nothingToCall(connector: Connector): string {
372 switch (connector.category) {
373 case "monitoring":
374 return "Opens issues by itself when something fires. Nothing for an agent to call.";
375 case "ai":
376 return "Runs models. Which ones an agent uses is set under Models on its profile.";
377 case "code":
378 return "Imports and mirrors repositories. Agents read the repositories themselves.";
379 default:
380 return "Nothing for an agent to call yet.";
381 }
382}
383
384/** The abilities one connector gives an agent; empty when it has none. */
385export function integrationAbilities(connectorId: string): AbilityDef[] {
386 return (INTEGRATION_ACTIONS[connectorId] ?? []).map((row) => ({
387 id: `integration:${connectorId}:${row.action}`,
388 group: "integration",
389 label: row.label,
390 about: row.about,
391 kind: row.kind,
392 tools: [row.tool],
393 status: "ready",
394 }));
395}
396
397/** The connectors among `connectors` that give agents abilities, in catalog order. */
398export function connectorsWithAbilities(connectors: Connector[]): Connector[] {
399 return connectors.filter((connector) => (INTEGRATION_ACTIONS[connector.id] ?? []).length > 0);
400}
401
402// MCP servers ------------------------------------------------------------
403
404/** An MCP tool as an ability of its server. */
405export function mcpAbility(server: Pick<McpServer, "id" | "name">, tool: McpTool): AbilityDef {
406 return {
407 id: `mcp:${server.id}:${tool.name}`,
408 group: "mcp",
409 label: tool.name,
410 about: tool.description || (tool.kind === "read" ? "Reads, as the server says." : "The server doesn't say it only reads, so it counts as a write."),
411 // Anything an MCP server changes happens outside g1t.
412 kind: tool.kind === "read" ? "read" : "send",
413 tools: [mcpToolName(server.name, tool.name)],
414 status: "ready",
415 };
416}
417
418/** The tool name an agent calls an MCP tool by: `<server>__<tool>`, in the characters the model API allows. */
419export function mcpToolName(server: string, tool: string): string {
420 const clean = (text: string) => text.toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
421 return `${clean(server)}__${clean(tool)}`.slice(0, 64);
422}
423
424/** An MCP server's name as kept: lowercase letters, digits and hyphens, 2 to 32. */
425export function checkMcpName(value: unknown): { ok: true; value: string } | { ok: false; message: string } {
426 const name = typeof value === "string" ? value.trim().toLowerCase() : "";
427 if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){1,31}$/.test(name)) return { ok: false, message: "A server's name is 2 to 32 lowercase letters, digits and single hyphens." };
428 return { ok: true, value: name };
429}
430
431/**
432 * An MCP server's address, checked: HTTPS, a public host name (not an
433 * address, not local, not g1t's own), no sign-in in the address.
434 */
435export function checkMcpUrl(value: unknown): { ok: true; value: string } | { ok: false; message: string } {
436 const text = typeof value === "string" ? value.trim() : "";
437 if (!text || text.length > 500) return { ok: false, message: "Give the server's HTTPS address." };
438 let url: URL;
439 try {
440 url = new URL(text);
441 } catch {
442 return { ok: false, message: "That isn't an address." };
443 }
444 if (url.protocol !== "https:") return { ok: false, message: "An MCP server is reached over HTTPS." };
445 if (url.username || url.password) return { ok: false, message: "Don't put a sign-in in the address." };
446 const host = url.hostname.toLowerCase();
447 const ip = /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.startsWith("[") || host.includes(":");
448 const local = host === "localhost" || /\.(local|localhost|internal|lan|home|arpa)$/.test(host) || !host.includes(".");
449 if (ip || local) return { ok: false, message: "Name the server by a public host name, not an address or a local name." };
450 if (/(^|\.)(g1t\.sh|g1tusercontent\.com)$/.test(host)) return { ok: false, message: "g1t's own addresses aren't MCP servers to add here." };
451 url.hash = "";
452 return { ok: true, value: url.toString() };
453}
454
455// Resolving --------------------------------------------------------------
456
457export type ResolveInput = {
458 /** The connector catalog (./connectors.ts `CONNECTORS`). */
459 connectors: Connector[];
460 abilities: AgentAbilities | null | undefined;
461 autonomy: AgentAutonomy;
462 /** Connector ids the workspace has connected. */
463 connected: string[];
464 /** Where a connector is set up or managed, by id; absent: nowhere to link. */
465 hrefs?: Record<string, string | null>;
466 /** Whether the agent is a member's personal one: then the workspace's connection isn't its to use unless an owner chose it. */
467 personal?: boolean;
468};
469
470function settingOf(input: ResolveInput, id: string): AbilitySetting {
471 return input.abilities?.settings?.[id] ?? {};
472}
473
474/** A definition as one agent has it: its level, within what the kind allows. */
475export function resolveOne(def: AbilityDef, input: ResolveInput, connected = true): Ability {
476 const setting = settingOf(input, def.id);
477 const max = maxLevel(def.kind);
478 let level: AbilityLevel;
479 let choices: AbilityLevel[];
480 let canChange: boolean;
481 if (def.autonomy) {
482 level = levelOfAutonomy(input.autonomy[def.autonomy.key]);
483 choices = def.autonomy.levels;
484 canChange = true;
485 } else if (def.group === "g1t") {
486 level = "alone";
487 choices = ["alone"];
488 canChange = false;
489 } else if (def.status === "coming") {
490 level = defaultLevel(def.kind);
491 choices = [];
492 canChange = false;
493 } else {
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.494 const wanted = setting.level ?? def.default ?? defaultLevel(def.kind);
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.495 level = withinLevel(wanted, max) ? wanted : max;
496 choices = ABILITY_LEVELS.filter((l) => withinLevel(l, max));
497 canChange = true;
498 }
499 const integration = def.group === "integration";
500 const connectorId = integration ? def.id.split(":")[1]! : null;
501 const connector = connectorId ? input.connectors.find((c) => c.id === connectorId) : null;
502 const personalAvailable = !!connector && connector.scopes.includes("personal") && (connector.personal?.status ?? connector.status) === "available";
503 return {
504 ...def,
505 level,
506 max,
507 choices,
508 can_change: canChange && (def.group !== "integration" || connected),
509 credentials: integration ? (setting.credentials ?? (input.personal ? "asker" : "workspace")) : null,
510 personal_available: personalAvailable,
511 };
512}
513
514/** The abilities an agent has, in sections, as the tab shows them and the service enforces them. */
515export function resolveAbilities(input: ResolveInput): AbilitySection[] {
516 const connected = new Set(input.connected.map((id) => id.toLowerCase()));
517 const hrefs = input.hrefs ?? {};
518 const sections: AbilitySection[] = [
519 {
520 group: "g1t",
521 title: "g1t",
522 about: "Always on, within the access of the person it acts for. Code and docs keep the choices below.",
523 sources: [{ id: "g1t", name: "g1t", connected: true, href: null, note: null, abilities: G1T_ABILITIES.map((def) => resolveOne(def, input)) }],
524 },
525 {
526 group: "computer",
527 title: "Its computer",
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.528 about: "A computer of its own on g1t cloud, used in its sessions: a shell and files now, a browser and web search coming. It sleeps when idle and keeps its home.",
529 sources: [{ id: "computer", name: "Computer", connected: true, href: null, note: null, abilities: COMPUTER_ABILITIES.map((def) => resolveOne(def, input)) }],
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.530 },
531 ];
532 // Every connected connector, then the ones with abilities that aren't connected yet.
533 const sources: AbilitySource[] = [];
534 for (const connector of input.connectors) {
535 if (connector.status !== "available" || !connector.scopes.includes("workspace")) continue;
536 const is = connected.has(connector.id);
537 const defs = integrationAbilities(connector.id);
538 if (!is && !defs.length) continue;
539 // Nothing an agent calls: not worth a row unless it is connected.
540 if (!defs.length && (connector.id === "webhooks" || connector.id === "ai-gateway")) continue;
541 sources.push({
542 id: connector.id,
543 name: connector.name,
544 connected: is,
545 href: hrefs[connector.id] ?? null,
546 note: defs.length ? null : nothingToCall(connector),
547 abilities: defs.map((def) => resolveOne(def, input, is)),
548 });
549 }
550 sources.sort((a, b) => Number(b.connected) - Number(a.connected));
551 sections.push({ group: "integration", title: "Integrations", about: "What it can do through what the workspace has connected, one row per action, and whose connection each runs on.", sources });
552 const servers = input.abilities?.mcp_servers ?? [];
553 sections.push({
554 group: "mcp",
555 title: "MCP servers",
556 about: "Servers an owner adds. Each tool the server lists is a row; a tool that doesn't say it only reads counts as a write.",
557 sources: servers.map((server) => ({
558 id: server.id,
559 name: server.name,
560 connected: true,
561 href: server.url,
562 note: server.problem ?? (server.tools.length ? null : "It listed no tools."),
563 abilities: server.tools.slice(0, MAX_MCP_TOOLS).map((tool) => resolveOne(mcpAbility(server, tool), input)),
564 })),
565 });
566 return sections;
567}
568
569/** Every ability in the sections, flat. */
570export function allAbilities(sections: AbilitySection[]): Ability[] {
571 return sections.flatMap((section) => section.sources.flatMap((source) => source.abilities));
572}
573
574/** The ability by id, among the sections; null when there is none. */
575export function findAbility(sections: AbilitySection[], id: string): { ability: Ability; source: AbilitySource } | null {
576 for (const section of sections) {
577 for (const source of section.sources) {
578 const ability = source.abilities.find((a) => a.id === id);
579 if (ability) return { ability, source };
580 }
581 }
582 return null;
583}
584
585/**
586 * Whether what the person said asked for this: the item's key (ENG-42), or
587 * the ability's own name, appears in it. For a level of `asked`.
588 */
589export function askedFor(said: string, keys: string[]): boolean {
590 const text = said.toLowerCase();
591 if (!text.trim()) return false;
592 return keys.some((key) => {
593 const needle = key.trim().toLowerCase();
594 return needle.length >= 2 && text.includes(needle);
595 });
596}
597
598/** A list in words: "a", "a and b", "a, b and c". */
599function list(items: string[]): string {
600 if (items.length <= 1) return items.join("");
601 return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
602}
603
604const lower = (text: string) => text.charAt(0).toLowerCase() + text.slice(1);
605
606/**
607 * The agent's abilities in a sentence or two: "Can read Linear issues and
608 * open pull requests on its own; imports Linear issues when asked for it;
609 * asks before commenting in Linear and merging; never deploys to
610 * production." Only what is connected and ready counts.
611 */
612export function abilitiesSummary(sections: AbilitySection[]): string {
613 const by: Record<AbilityLevel, string[]> = { alone: [], asked: [], ask: [], never: [] };
614 for (const section of sections) {
615 for (const source of section.sources) {
616 if (!source.connected) continue;
617 for (const ability of source.abilities) {
618 if (ability.status !== "ready") continue;
619 // g1t's always-on reads go without saying; its choices and everything outside are the point.
620 if (section.group === "g1t" && !ability.autonomy) continue;
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.621 const what =
622 section.group === "g1t"
623 ? lower(ability.label)
624 : section.group === "computer"
625 ? computerWords(ability.id)
626 : section.group === "mcp"
627 ? `call ${ability.label} on ${source.name}`
628 : `${lower(ability.label)} in ${source.name}`;
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.629 by[ability.level].push(what);
630 }
631 }
632 }
633 const parts: string[] = [];
634 if (by.alone.length) parts.push(`can ${list(by.alone)} on its own`);
635 if (by.asked.length) parts.push(`${list(by.asked.map(verb))} when asked for it`);
636 if (by.ask.length) parts.push(`asks before ${list(by.ask.map(gerund))}`);
637 if (by.never.length) parts.push(`never ${list(by.never.map(verb))}`);
638 if (!parts.length) return "Reads code, chat, issues and artifacts within the asker's access; nothing outside g1t yet.";
639 const text = parts.join("; ");
640 return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
641}
642
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.643/** A computer ability as what the agent does with it. */
644function computerWords(id: string): string {
645 return id === "computer:shell" ? "run commands on its computer" : "change files on its computer";
646}
647
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.648/** "read issues in Linear" → "reads issues in Linear". */
649function verb(what: string): string {
650 const [first, ...rest] = what.split(" ");
651 if (!first) return what;
652 const third = first.endsWith("s") ? first : first.endsWith("y") && !/[aeiou]y$/.test(first) ? `${first.slice(0, -1)}ies` : `${first}s`;
653 return [third, ...rest].join(" ");
654}
655
656/** "comment in Linear" → "commenting in Linear". */
657function gerund(what: string): string {
658 const [first, ...rest] = what.split(" ");
659 if (!first) return what;
660 const ing = first.endsWith("e") && first !== "be" ? `${first.slice(0, -1)}ing` : first.endsWith("ing") ? first : `${first}ing`;
661 return [ing, ...rest].join(" ");
662}
663
664/** The settings with one ability's level or credentials changed, keeping only what differs from the default. */
665export function withSetting(abilities: AgentAbilities | null | undefined, id: string, change: AbilitySetting): AgentAbilities {
666 const base = abilities ?? EMPTY_ABILITIES;
667 const current = { ...(base.settings[id] ?? {}) };
668 if (change.level !== undefined) current.level = change.level;
669 if (change.credentials !== undefined) current.credentials = change.credentials;
670 const next = { ...base.settings };
671 const kept: AbilitySetting = {};
672 if (current.level) kept.level = current.level;
673 if (current.credentials) kept.credentials = current.credentials;
674 if (Object.keys(kept).length) next[id] = kept;
675 else delete next[id];
676 return { settings: next, mcp_servers: base.mcp_servers ?? [] };
677}