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 | */ | |
| 32 | import type { Connector } from "./connectors"; | |
| 33 | import type { AgentAutonomy } from "./workspace-agents"; | |
| 34 | ||
| 35 | /** Alone; alone when the asker asked for it; ask first; never. */ | |
| 36 | export type AbilityLevel = "alone" | "asked" | "ask" | "never"; | |
| 37 | ||
| 38 | export const ABILITY_LEVELS: readonly AbilityLevel[] = ["alone", "asked", "ask", "never"]; | |
| 39 | ||
| 40 | export 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. */ | |
| 48 | const ORDER: Record<AbilityLevel, number> = { alone: 0, asked: 1, ask: 2, never: 3 }; | |
| 49 | ||
| 50 | export 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 | */ | |
| 57 | export type AbilityKind = "read" | "write" | "send" | "restricted"; | |
| 58 | ||
| 59 | /** Whose connection an integration ability runs on. */ | |
| 60 | export type AbilityCredentials = "workspace" | "asker"; | |
| 61 | ||
| 62 | export type AbilityStatus = "ready" | "coming"; | |
| 63 | ||
| 64 | /** The level a kind starts at. */ | |
| 65 | export 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. */ | |
| 79 | export function maxLevel(kind: AbilityKind): AbilityLevel { | |
| 80 | return kind === "restricted" ? "ask" : "alone"; | |
| 81 | } | |
| 82 | ||
| 83 | /** Whether `level` is within `max`: no freer than it. */ | |
| 84 | export function withinLevel(level: AbilityLevel, max: AbilityLevel): boolean { | |
| 85 | return ORDER[level] >= ORDER[max]; | |
| 86 | } | |
| 87 | ||
| 88 | /** One ability as the catalog defines it. */ | |
| 89 | export 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. */ | |
| 110 | export type AbilitySetting = { level?: AbilityLevel | null; credentials?: AbilityCredentials | null }; | |
| 111 | ||
| 112 | /** A tool an MCP server lists, as it was found. */ | |
| 113 | export 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. */ | |
| 123 | export 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. */ | |
| 140 | export 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 | ||
| 146 | export 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. */ | |
| 149 | export type AgentAbilitiesChange = { settings: Record<string, AbilitySetting> }; | |
| 150 | ||
| 151 | /** The most MCP servers one agent has, and the most tools one server lists. */ | |
| 152 | export const MAX_MCP_SERVERS = 10; | |
| 153 | export const MAX_MCP_TOOLS = 40; | |
| 154 | ||
| 155 | /** An ability as one agent has it now. */ | |
| 156 | export 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. */ | |
| 171 | export 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 | ||
| 183 | export 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. */ | |
| 191 | export 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. */ | |
| 207 | export 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. */ | |
| 305 | export 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 | ||
| 317 | export 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. | 331 | export 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. */ |
| 339 | export 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 | */ | |
| 351 | const 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. */ | |
| 371 | function 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. */ | |
| 385 | export 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. */ | |
| 398 | export 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. */ | |
| 405 | export 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. */ | |
| 419 | export 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. */ | |
| 425 | export 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 | */ | |
| 435 | export 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 | ||
| 457 | export 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 | ||
| 470 | function 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. */ | |
| 475 | export 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. */ | |
| 515 | export 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. */ | |
| 570 | export 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. */ | |
| 575 | export 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 | */ | |
| 589 | export 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". */ | |
| 599 | function 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 | ||
| 604 | const 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 | */ | |
| 612 | export 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. */ |
| 644 | function 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". */ |
| 649 | function 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". */ | |
| 657 | function 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. */ | |
| 665 | export 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 | } |