Skip to content
438 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 at work (docs.g1t.sh/guides/agent-abilities/): what
3 * the workspace has connected, the agent's level for each ability
4 * (@g1t/contracts abilities.ts), and the ports that carry a call out once
5 * the tool box's gate allowed it: the integrations service, an MCP server,
6 * and the cards in chat that ask first, offer to connect, or request
7 * something from the owners.
8 *
9 * Asked first: the call is kept in `agent_ability_requests` with its
10 * arguments and a card is posted. Whoever may allow it (the person the
11 * agent acts for, or an owner) presses Allow, the call runs as them, and
12 * the agent hears the result: a session reads it at its next step
13 * (cards.ts). Every refusal names its rule in the transcript, through the
14 * tool's result, and in the audit log, through `refused`.
15 */
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r916import { type AbilitySection, type McpServer, type MessageCard, type ServiceBinding, type User, chatClient, identityClient, integrationsClient, newId, notifyClient } from "@g1t/contracts";
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.17
18import { CONNECTORS, connectorPath, connectorView } from "../../../packages/contracts/src/connectors.ts";
19import { findAbility, resolveAbilities } from "../../../packages/contracts/src/abilities.ts";
20import { abilityCard, connectCard, requestCard } from "./card-views.ts";
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.21import { computerPorts } from "./computer.ts";
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.22export { abilitiesSection, saidText } from "./abilities-prompt.ts";
23import type { Definition } from "./definition.ts";
24import { callMcpTool } from "./mcp-client.ts";
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r925import { type ConnectorRow, type Deps as ConnectorDeps, asServer, headersFor, reaching, sealingKey, stateOf } from "./connectors.ts";
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.26import { type Row, isPersonal } from "./store.ts";
27import type { AbilityPorts, OutsideDone, OutsideItem } from "./tools.ts";
28
29export type AbilityEnv = {
30 DB: D1Database;
31 INTEGRATIONS: ServiceBinding;
32 CHAT: ServiceBinding;
33 IDENTITY: ServiceBinding;
34 EVENTS: ServiceBinding;
35 NOTIFY?: ServiceBinding;
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.36 /** The runner, for a call on the agent's computer allowed from a card (computer.ts). */
37 RUNNER?: ServiceBinding;
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.38 /** The site's address (https://g1t.sh), for links that leave g1t. */
39 SITE_URL?: string;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r940 /** Seals connectors' secrets (connectors.ts); without it, connectors that sign in can't be used. */
41 CONNECTORS_KEY?: string;
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.42};
43
44/** A call waiting to be allowed, as kept. */
45export type AbilityRequestRow = {
46 id: string;
47 agent_id: string;
48 workspace_id: string;
49 workspace: string;
50 channel_id: string;
51 message_id: string | null;
52 session_id: string | null;
53 ability: string;
54 tool: string;
55 input: string;
56 summary: string;
57 asked_by: string | null;
58 status: string;
59 decided_by: string | null;
60 decided_at: string | null;
61 result: string | null;
62 created_at: string;
63 updated_at: string;
64};
65
66const iso = () => new Date().toISOString();
67
68/** The site's address, without a trailing slash. */
69export function siteUrl(env: { SITE_URL?: string }): string {
70 return (env.SITE_URL ?? "").trim().replace(/\/+$/, "") || "https://g1t.sh";
71}
72
73/**
74 * The connector ids the workspace has connected, as the integrations
75 * service lists its connections to a member. Empty when it can't say.
76 */
77export async function connectedConnectors(env: Pick<AbilityEnv, "INTEGRATIONS">, workspace: string, viewer: User): Promise<string[]> {
78 const listed = await integrationsClient(env.INTEGRATIONS)
79 .list(workspace, viewer)
80 .catch(() => null);
81 if (!listed?.ok) return [];
82 const providers = new Set(listed.value.map((connection) => connection.provider));
83 return CONNECTORS.filter((connector) => connector.provider && providers.has(connector.provider)).map((connector) => connector.id);
84}
85
86/** The agent's abilities for a turn: resolved against what is connected. */
87export async function abilitiesFor(env: Pick<AbilityEnv, "INTEGRATIONS">, input: { agent: Row; definition: Definition; workspace: string; asker: User }): Promise<AbilitySection[]> {
88 const connected = await connectedConnectors(env, input.workspace, input.asker);
89 return resolveAbilities({ connectors: CONNECTORS, abilities: input.definition.abilities, autonomy: input.definition.autonomy, connected, personal: isPersonal(input.agent) });
90}
91
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r992/** What the connectors' secrets are sealed with, and where sign-ins come back to. */
93export async function connectorDeps(env: Pick<AbilityEnv, "CONNECTORS_KEY" | "SITE_URL">): Promise<ConnectorDeps> {
94 return { key: await sealingKey(env.CONNECTORS_KEY), site: siteUrl(env) };
95}
96
97/**
98 * The agent's abilities for a turn, with the workspace's connectors that
99 * reach it (docs.g1t.sh/guides/customize/, "Connectors") joined to its own
100 * MCP servers: every connected connector is an MCP source like them, so
101 * levels, Ask-first cards and the audit log treat both alike. `servers`
102 * is what the tool box is given; `connectors` the rows behind them, for
103 * their sign-ins.
104 */
105export async function abilitiesWithConnectors(
106 env: Pick<AbilityEnv, "INTEGRATIONS" | "DB">,
107 input: { agent: Row; definition: Definition; workspace: string; asker: User; teams: readonly string[] },
108): Promise<{ sections: AbilitySection[]; servers: McpServer[]; connectors: Map<string, ConnectorRow> }> {
109 const own = input.definition.abilities.mcp_servers ?? [];
110 const reach = await reaching(env.DB, input.agent.workspace_id, input.agent.id, input.teams).catch((error: unknown) => {
111 console.error("agents: connectors not read", input.agent.id, String(error));
112 return [] as Awaited<ReturnType<typeof reaching>>;
113 });
114 const rows = new Map<string, ConnectorRow>();
115 const servers: McpServer[] = [...own];
116 for (const { row } of reach) {
117 if (stateOf(row) !== "connected") continue;
118 rows.set(row.id, row);
119 servers.push(asServer(row));
120 }
121 const connected = await connectedConnectors(env, input.workspace, input.asker);
122 const sections = resolveAbilities({ connectors: CONNECTORS, abilities: { ...input.definition.abilities, mcp_servers: servers }, autonomy: input.definition.autonomy, connected, personal: isPersonal(input.agent) });
123 return { sections, servers, connectors: rows };
124}
125
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.126/** Where a request's card and a session's wait point: the Abilities tab. */
127export function abilitiesPath(workspace: string, handle: string): string {
128 return `/${workspace}/-/agents/${handle}/abilities`;
129}
130
131const item = (c: { provider: string; key: string; title: string; url: string; status: string | null; body: string }): OutsideItem => ({ provider: c.provider, key: c.key, title: c.title, url: c.url, status: c.status, body: c.body });
132
133const outcome = <T, U>(result: { ok: true; value: T } | { ok: false; error: { code: string; message: string } }, map: (value: T) => U): OutsideDone<U> =>
134 result.ok ? { ok: true, value: map(result.value) } : { ok: false, code: result.error.code, message: result.error.message };
135
136/**
137 * The ports for one turn. `postCard` posts where the agent is working (a
138 * reply's conversation, a session's thread) and gives the message id.
139 */
140export function abilityPorts(
141 env: AbilityEnv,
142 input: {
143 agent: Row;
144 workspace: string;
145 channel_id: string;
146 session: { id: string; title: string } | null;
147 asker: { id: string | null; username: string | null };
148 postCard: (card: MessageCard) => Promise<string | null>;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9149 /** The workspace's connectors behind the agent's MCP servers, by server id (abilitiesWithConnectors). */
150 connectors?: Map<string, ConnectorRow>;
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.151 },
152): AbilityPorts {
153 const integrations = integrationsClient(env.INTEGRATIONS);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9154 let deps: Promise<ConnectorDeps> | null = 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.155 const { agent, workspace } = input;
156 const link = `${siteUrl(env)}/${workspace}/-/chat/${input.channel_id}`;
157 return {
158 lookup: async (asker, reference) => outcome(await integrations.resolve(workspace, asker, reference), item),
159 import: async (asker, repo, reference) =>
160 outcome(await integrations.import(asker, { namespace: repo.namespace, name: repo.name }, reference, false), (v) => ({ number: v.number, item: item(v.item), created: v.created })),
161 act: async (asker, reference, action, text) => {
162 const signed = `${text}\n\n— ${agent.display_name} (@${agent.handle}), a g1t agent, for @${asker.username}.`;
163 const done = action === "resolve" ? await integrations.close(asker, workspace, reference, signed, link) : await integrations.comment(asker, workspace, reference, signed, link);
164 return outcome(done, item);
165 },
166 // Personal connections to Linear, Jira and Sentry aren't available yet (connectors.ts says so), so nobody has one.
167 askerConnected: async () => false,
168 mcp: async (server, tool, args) => {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9169 let headers: Record<string, string> = {};
170 const row = input.connectors?.get(server.id);
171 if (row) {
172 deps ??= connectorDeps(env);
173 const signed = await headersFor(env.DB, await deps, row);
174 if (!signed) return { ok: false, code: "error", message: `${row.title} needs to be signed in again by an owner (Customize → Connectors).` };
175 headers = signed;
176 }
177 const done = await callMcpTool(server.url, tool.name, args, undefined, headers);
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.178 return done.ok ? { ok: true, value: done.text } : { ok: false, code: "error", message: done.message };
179 },
180 async askFirst({ ability, source, tool, args, summary, note }) {
181 const id = newId("abr");
182 const now = iso();
183 const row: AbilityRequestRow = {
184 id,
185 agent_id: agent.id,
186 workspace_id: agent.workspace_id,
187 workspace,
188 channel_id: input.channel_id,
189 message_id: null,
190 session_id: input.session?.id ?? null,
191 ability: ability.id,
192 tool,
193 input: JSON.stringify(args).slice(0, 20_000),
194 summary: summary.slice(0, 300),
195 asked_by: input.asker.id,
196 status: "pending",
197 decided_by: null,
198 decided_at: null,
199 result: null,
200 created_at: now,
201 updated_at: now,
202 };
203 await env.DB.prepare(
204 `INSERT INTO agent_ability_requests (id, agent_id, workspace_id, workspace, channel_id, session_id, ability, tool, input, summary, asked_by, status, created_at, updated_at)
205 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`,
206 )
207 .bind(id, row.agent_id, row.workspace_id, row.workspace, row.channel_id, row.session_id, row.ability, row.tool, row.input, row.summary, row.asked_by, now, now)
208 .run();
209 const messageId = await input.postCard(abilityCard(row, { agent: agent.display_name, asker: input.asker.username, rule: `${source.name}: ${ability.label}`, level: ability.level, note, body: bodyOf(args) }));
210 if (!messageId) {
211 await env.DB.prepare("UPDATE agent_ability_requests SET status = 'failed', result = ?, updated_at = ? WHERE id = ?").bind("The card couldn't be posted.", iso(), id).run();
212 return null;
213 }
214 await env.DB.prepare("UPDATE agent_ability_requests SET message_id = ? WHERE id = ?").bind(messageId, id).run();
215 return id;
216 },
217 async connect(source, ability) {
218 const connector = CONNECTORS.find((c) => c.id === source.id);
219 const view = connector ? connectorView(connector, "personal") : null;
220 const href = view?.href ? connectorPath(view.href, workspace) : "/settings/integrations";
221 const messageId = await input.postCard(connectCard({ connector: source.name, agent: agent.display_name, ability: ability.label, asker: input.asker.username, href }));
222 return messageId !== null;
223 },
224 async request({ connector, ability, source, why }) {
225 const found = connector ? CONNECTORS.find((c) => c.id === connector) : null;
226 if (!ability && !found) return false;
227 const card = requestCard({
228 agent: { id: agent.id, handle: agent.handle, display_name: agent.display_name },
229 workspace,
230 connector: found ? { id: found.id, name: found.name, available: found.status === "available" && found.scopes.includes("workspace") } : null,
231 ability: ability && source ? { id: ability.id, label: `${source.name}: ${ability.label}` } : null,
232 why,
233 status: "open",
234 by: null,
235 });
236 return (await input.postCard(card)) !== null;
237 },
238 refused({ ability, source, rule, call }) {
239 recordRefusal(env, { agent, workspace, asker: input.asker.username, rule, message: `Refused "${call}": ${source.name}: ${ability.label} is ${rule.split("=")[1] ?? ability.level}.` });
240 },
241 };
242}
243
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.244/** A card's preview of what a call would write or run: the text of a comment or note, or the command. */
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.245function bodyOf(args: Record<string, unknown>): string | 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.246 const text = typeof args.text === "string" ? args.text.trim() : typeof args.command === "string" ? `$ ${args.command.trim()}` : "";
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.247 return text ? text.slice(0, 900) : null;
248}
249
250/**
251 * A refusal in the workspace's audit log, by the agent, naming the rule
252 * (`integration:linear:comment=never`). Never fails the turn.
253 */
254export function recordRefusal(env: Pick<AbilityEnv, "EVENTS">, input: { agent: Row; workspace: string; asker: string | null; rule: string; message: string }): void {
255 const entry = {
256 actorKind: "agent",
257 actor: input.agent.handle,
258 actorId: input.agent.id,
259 agent: input.agent.handle,
260 onBehalfOf: input.asker,
261 runId: null,
262 runKind: null,
263 credentialId: null,
264 action: "ability_refused",
265 // The audit log knows the surfaces people use; an agent acts through the site on their behalf.
266 surface: "web",
267 workspace: input.workspace.toLowerCase(),
268 repo: null,
269 number: null,
270 gitRef: null,
271 path: `agents/${input.agent.handle}`,
272 outcome: "denied",
273 rule: input.rule,
274 result: "refused",
275 message: input.message,
276 requestId: `req_${crypto.randomUUID()}`,
277 };
278 env.EVENTS.fetch("https://service/rpc/audit_record", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ entries: [entry] }) })
279 .then((response) => {
280 if (!response.ok) throw new Error(`status ${response.status}`);
281 })
282 .catch((error: unknown) => console.error("agents: a refusal was not written to the audit log", String(error)));
283}
284
285/** An allowed or denied call in the audit log, by the person who decided. */
286export function recordDecision(env: Pick<AbilityEnv, "EVENTS">, input: { by: User; agent: Row; workspace: string; request: AbilityRequestRow; allowed: boolean }): void {
287 const entry = {
288 actorKind: "person",
289 actor: input.by.username,
290 actorId: input.by.id,
291 agent: input.agent.handle,
292 onBehalfOf: null,
293 runId: null,
294 runKind: null,
295 credentialId: null,
296 action: input.allowed ? "ability_allowed" : "ability_denied",
297 surface: "web",
298 workspace: input.workspace.toLowerCase(),
299 repo: null,
300 number: null,
301 gitRef: null,
302 path: `agents/${input.agent.handle}`,
303 outcome: "allowed",
304 rule: `${input.request.ability}=ask`,
305 result: "ok",
306 message: `${input.allowed ? "Allowed" : "Denied"} @${input.agent.handle}: ${input.request.summary}`,
307 requestId: `req_${crypto.randomUUID()}`,
308 };
309 env.EVENTS.fetch("https://service/rpc/audit_record", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ entries: [entry] }) })
310 .then((response) => {
311 if (!response.ok) throw new Error(`status ${response.status}`);
312 })
313 .catch((error: unknown) => console.error("agents: a decision was not written to the audit log", String(error)));
314}
315
316/** Pending requests of a session: what it waits on. */
317export async function pendingRequests(db: D1Database, sessionId: string): Promise<AbilityRequestRow[]> {
318 const { results } = await db.prepare("SELECT * FROM agent_ability_requests WHERE session_id = ? AND status = 'pending' ORDER BY created_at").bind(sessionId).all<AbilityRequestRow>();
319 return results;
320}
321
322/**
323 * Runs an allowed call as `by`, the person who allowed it, with the
324 * agent's abilities as they are now (a server or a connection may have
325 * gone since). What the agent is told.
326 */
327export async function runAllowed(env: AbilityEnv, request: AbilityRequestRow, agent: Row, definition: Definition, by: User): Promise<{ ok: boolean; message: string }> {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9328 const teams = await identityClient(env.IDENTITY)
329 .agentTeams(request.workspace, agent.id)
330 .then((t) => t.map((x) => x.slug))
331 .catch(() => [] as string[]);
332 const { sections, servers, connectors } = await abilitiesWithConnectors(env, { agent, definition, workspace: request.workspace, asker: by, teams });
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.333 const found = findAbility(sections, request.ability);
334 if (!found) return { ok: false, message: `The ability ${request.ability} is no longer there.` };
335 if (!found.source.connected) return { ok: false, message: `${found.source.name} is no longer connected.` };
336 if (found.ability.level === "never") return { ok: false, message: `${found.source.name}: ${found.ability.label} is Never now.` };
337 let args: Record<string, unknown> = {};
338 try {
339 args = JSON.parse(request.input) as Record<string, unknown>;
340 } catch {
341 return { ok: false, message: "The call's arguments couldn't be read." };
342 }
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9343 const ports = abilityPorts(env, { agent, workspace: request.workspace, channel_id: request.channel_id, session: null, asker: { id: by.id, username: by.username }, postCard: async () => null, connectors });
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.344 const reference = String(args.reference ?? "").trim();
345 try {
346 switch (request.tool) {
347 case "lookup_outside": {
348 const done = await ports.lookup(by, reference);
349 return done.ok ? { ok: true, message: `${done.value.key}: ${done.value.title}${done.value.status ? ` [${done.value.status}]` : ""}\n${done.value.url}\n\n${done.value.body}`.slice(0, 20_000) } : { ok: false, message: done.message };
350 }
351 case "import_outside": {
352 const repo = String(args.repo ?? "").trim().toLowerCase();
353 const [namespace, name] = repo.includes("/") ? repo.split("/") : [request.workspace, repo];
354 if (!namespace || !name) return { ok: false, message: "The call named no repository." };
355 const done = await ports.import(by, { id: "", namespace, name, isPrivate: true, defaultBranch: "main" }, reference);
356 return done.ok ? { ok: true, message: `${done.value.created ? "Opened" : "Already imported as"} ${namespace}/${name}#${done.value.number} from ${done.value.item.key}.` } : { ok: false, message: done.message };
357 }
358 case "act_outside": {
359 const action = args.action === "resolve" ? "resolve" : "comment";
360 const done = await ports.act(by, reference, action, String(args.text ?? "").trim().slice(0, 8000));
361 return done.ok ? { ok: true, message: `${action === "resolve" ? "Resolved" : "Commented on"} ${done.value.key} (${done.value.url}).` } : { ok: false, message: done.message };
362 }
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.363 case "run_command":
364 case "computer_read_file":
365 case "computer_write_file": {
366 // On the agent's own computer, in the session that asked (or a directory of its own for a reply's card).
367 const computer = computerPorts(env, { agent, workspace: request.workspace, session: { id: request.session_id ?? `card-${request.id}` }, asker: { username: by.username }, onCommand: () => undefined });
368 if (!computer) return { ok: false, message: "Agents' computers aren't available on this installation." };
369 const path = String(args.path ?? "").trim();
370 if (request.tool === "run_command") {
371 const command = String(args.command ?? "").trim();
372 if (!command) return { ok: false, message: "The call named no command." };
373 const timeout = Number.isFinite(Number(args.timeout_seconds)) && Number(args.timeout_seconds) > 0 ? Math.min(1800, Math.floor(Number(args.timeout_seconds))) : null;
374 const ran = await computer.exec(command, typeof args.cwd === "string" && args.cwd.trim() ? args.cwd.trim() : null, timeout);
375 if (!ran.ok) return { ok: false, message: ran.message };
376 const how = [`exit ${ran.value.exit_code}`, `${(ran.value.duration_ms / 1000).toFixed(1)} s`, ran.value.timed_out ? "killed at the timeout" : null, ran.value.truncated ? "output cut" : null].filter(Boolean).join(", ");
377 return { ok: ran.value.exit_code === 0, message: `$ ${ran.value.cmd}\n(${how}; in ${ran.value.cwd})\n${ran.value.output || "(no output)"}`.slice(0, 20_000) };
378 }
379 if (!path) return { ok: false, message: "The call named no path." };
380 if (request.tool === "computer_read_file") {
381 const read = await computer.readFile(path);
382 return read.ok ? { ok: true, message: read.value.text.slice(0, 20_000) } : { ok: false, message: read.message };
383 }
384 const wrote = await computer.writeFile(path, String(args.text ?? ""));
385 return wrote.ok ? { ok: true, message: `Wrote ${wrote.value.bytes} bytes to ${wrote.value.path}.` } : { ok: false, message: wrote.message };
386 }
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.387 default: {
388 // An MCP tool: `<server>__<tool>`.
389 const [, serverId, ...rest] = request.ability.split(":");
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9390 const server = servers.find((s: McpServer) => s.id === serverId);
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.391 const tool = server?.tools.find((t) => t.name === rest.join(":"));
392 if (!server || !tool) return { ok: false, message: "That MCP tool is no longer there." };
393 const done = await ports.mcp(server, tool, args);
394 return done.ok ? { ok: true, message: done.value.slice(0, 20_000) } : { ok: false, message: done.message };
395 }
396 }
397 } catch (error) {
398 return { ok: false, message: `It failed: ${String(error).slice(0, 200)}` };
399 }
400}
401
402/** Tells the owners (and whoever asked, for a request on their behalf) that a Request card was pressed. */
403export async function tellOwnersOfRequest(env: AbilityEnv, input: { workspace: string; by: User; title: string; body: string; href: string; id: string }): Promise<void> {
404 if (!env.NOTIFY) return;
405 const members = await identityClient(env.IDENTITY)
406 .listMembers(input.workspace, input.by)
407 .catch(() => null);
408 if (!members?.ok) return;
409 const notify = notifyClient(env.NOTIFY);
410 const owners = members.value.filter((member) => member.role === "owner").slice(0, 20);
411 await Promise.all(
412 owners.map((owner) =>
413 notify
414 .notify(
415 { username: owner.username },
416 {
417 id: `ability-request:${input.id}:${owner.username}`,
418 kind: "approval",
419 workspace: input.workspace,
420 title: input.title,
421 body: input.body,
422 href: input.href,
423 actor: { kind: "user", id: input.by.id, name: input.by.username, avatar: input.by.avatar ?? null, avatar_seed: null },
424 created_at: iso(),
425 },
426 )
427 .catch(() => undefined),
428 ),
429 );
430}
431
432/** Posts a card in a conversation as the agent; its message id, or null. */
433export async function postCardAsAgent(env: Pick<AbilityEnv, "CHAT">, workspace: string, channelId: string, agentId: string, card: MessageCard, threadRoot: string | null, askedBy: string | null): Promise<string | null> {
434 const posted = await chatClient(env.CHAT)
435 .postAsAgent(workspace, channelId, agentId, { body: "", card, thread_root: threadRoot, asked_by: askedBy })
436 .catch(() => null);
437 return posted?.ok ? posted.value.id : null;
438}

This file's history is long; its oldest lines are credited to the oldest commit read.