Skip to content
364 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.1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { AbilitySection, McpServer, User } from "@g1t/contracts";
5
6import { resolveAbilities, withSetting } from "../../../packages/contracts/src/abilities.ts";
7import { CONNECTORS } from "../../../packages/contracts/src/connectors.ts";
8import { abilitiesSection, saidText } from "./abilities-prompt.ts";
9import { Audience, type AudienceInfo, type AudiencePorts, type RepoRef } from "./audience.ts";
10import { abilityCard, connectCard, requestCard } from "./card-views.ts";
11import { DEFAULT_AUTONOMY, applyChanges } from "./definition.ts";
12import { callMcpTool, listMcpTools } from "./mcp-client.ts";
13import { type AbilityPorts, type ActionPorts, type ToolPorts, ToolBox } from "./tools.ts";
14
15// ── A small world ────────────────────────────────────────────────────────
16
17const WEB: RepoRef = { id: "rep_web", namespace: "acme", name: "web", isPrivate: true, defaultBranch: "main" };
18const person = (id: string): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member" }] }) as User;
19
20function world(): AudiencePorts {
21 const info: AudienceInfo = { kind: "dm", member_user_ids: ["asker"], member_count: 1 };
22 return {
23 info: async () => info,
24 users: async (ids) => [person("asker")].filter((u) => ids.includes(u.id)),
25 workspaceRepos: async () => [WEB],
26 readable: async (ids) => [WEB].filter((r) => ids.includes(r.id)),
27 };
28}
29
30const ports: ToolPorts = {
31 readFile: async () => null,
32 searchCode: async () => [],
33 listIssues: async () => [],
34 getIssue: async () => null,
35 getPull: async () => null,
36 recentPulls: async () => [],
37 searchMessages: async () => [],
38 readThread: async () => null,
39 roster: async () => "",
40 consult: async () => ({ ok: false, message: "no" }),
41};
42
43const actions: ActionPorts = {
44 remember: async () => ({ ok: true, message: "" }),
45 forget: async () => ({ ok: true, message: "" }),
46 draftIssue: async () => ({ ok: true, message: "" }),
47};
48
49const ITEMS: Record<string, { provider: string; key: string; title: string; url: string; status: string | null; body: string }> = {
50 "ENG-42": { provider: "linear", key: "ENG-42", title: "Retry flaky checks", url: "https://linear.app/acme/issue/ENG-42", status: "In Progress", body: "The queue retries." },
51 "TECH-7": { provider: "jira", key: "TECH-7", title: "Login timeout", url: "https://acme.atlassian.net/browse/TECH-7", status: "To Do", body: "Times out." },
52};
53
54type Posted = { kind: string; title: string };
55
56/** Ports that record what they were asked to do. */
57function fakeAbilityPorts(over: Partial<AbilityPorts> = {}): AbilityPorts & { posted: Posted[]; acted: string[]; refusals: string[] } {
58 const posted: Posted[] = [];
59 const acted: string[] = [];
60 const refusals: string[] = [];
61 return {
62 posted,
63 acted,
64 refusals,
65 lookup: async (_asker, reference) => (ITEMS[reference] ? { ok: true, value: ITEMS[reference]! } : { ok: false, code: "not_found", message: "None of the acme workspace's integrations knows that." }),
66 import: async (_asker, repo, reference) => ({ ok: true, value: { number: 7, item: ITEMS[reference]!, created: true } }),
67 act: async (_asker, reference, action, text) => {
68 acted.push(`${action} ${reference}: ${text}`);
69 return { ok: true, value: ITEMS[reference]! };
70 },
71 askerConnected: async () => false,
72 mcp: async (server, tool, args) => {
73 acted.push(`mcp ${server.name}.${tool.name} ${JSON.stringify(args)}`);
74 return { ok: true, value: "sunny" };
75 },
76 askFirst: async ({ summary }) => {
77 posted.push({ kind: "ability", title: summary });
78 return "abr_1";
79 },
80 connect: async (source) => {
81 posted.push({ kind: "connect", title: source.name });
82 return true;
83 },
84 request: async ({ connector, ability }) => {
85 posted.push({ kind: "request", title: connector ?? ability?.id ?? "" });
86 return true;
87 },
88 refused: ({ rule }) => {
89 refusals.push(rule);
90 },
91 ...over,
92 };
93}
94
95const SERVER: McpServer = {
96 id: "mcp_w",
97 name: "weather",
98 url: "https://mcp.example.com/",
99 tools: [
100 { name: "get_forecast", description: "Today's forecast", kind: "read", input_schema: { type: "object", properties: { city: { type: "string" } } } },
101 { name: "set_alert", description: "Set an alert", kind: "write", input_schema: { type: "object", properties: {} } },
102 ],
103 added_by: "ana",
104 added_at: "2026-10-10T00:00:00.000Z",
105 checked_at: null,
106 problem: null,
107};
108
109async function box(input: { connected?: string[]; settings?: Record<string, { level?: "alone" | "asked" | "ask" | "never"; credentials?: "workspace" | "asker" }>; servers?: McpServer[]; said?: string; session?: boolean; ports?: Partial<AbilityPorts> }) {
110 const audience = await Audience.build("acme", "asker", world());
111 const tools = new ToolBox(audience, ports, { agentId: "agt_me", notConsult: ["me"], hops: 0, maxHops: 6, session: input.session }, [], actions);
112 let abilities = { settings: {}, mcp_servers: input.servers ?? [] };
113 for (const [id, setting] of Object.entries(input.settings ?? {})) abilities = withSetting(abilities, id, setting);
114 const sections: AbilitySection[] = resolveAbilities({ connectors: CONNECTORS, abilities, autonomy: DEFAULT_AUTONOMY, connected: input.connected ?? [] });
115 const fake = fakeAbilityPorts(input.ports);
116 tools.useAbilities(sections, fake, input.said ?? "", input.servers ?? []);
117 return { tools, fake, sections };
118}
119
120const names = (tools: ToolBox) => tools.definitions().map((t) => t.name);
121
122// ── Offering ─────────────────────────────────────────────────────────────
123
124test("nothing connected: no outside tools but request_ability; a connected Linear offers lookup, import and act", async () => {
125 const none = await box({});
126 assert.ok(!names(none.tools).includes("lookup_outside"));
127 assert.ok(names(none.tools).includes("request_ability"), "it can still ask for what it lacks");
128 const linear = await box({ connected: ["linear"] });
129 for (const tool of ["lookup_outside", "import_outside", "act_outside"]) assert.ok(names(linear.tools).includes(tool), tool);
130});
131
132test("an ability set to Never isn't offered when it is the only one for its tool, and is refused with the rule if called anyway", async () => {
133 const { tools, fake } = await box({ connected: ["linear"], settings: { "integration:linear:comment": { level: "never" } } });
134 assert.ok(!names(tools).includes("act_outside"), "Linear's only act ability is Never");
135 const tried = await tools.run("act_outside", { reference: "ENG-42", action: "comment", text: "hi" });
136 assert.equal(tried.outcome, "refused");
137 assert.match(tried.text, /no tool called act_outside/);
138 assert.deepEqual(fake.acted, []);
139});
140
141test("Never on one system, allowed on another: the tool is offered, and the rule is named per call, in the result and the audit log", async () => {
142 const { tools, fake } = await box({ connected: ["linear", "jira"], settings: { "integration:linear:comment": { level: "never" }, "integration:jira:comment": { level: "alone" } } });
143 assert.ok(names(tools).includes("act_outside"));
144 const linear = await tools.run("act_outside", { reference: "ENG-42", action: "comment", text: "Looks fixed." });
145 assert.equal(linear.outcome, "refused");
146 assert.match(linear.text, /Not allowed: your abilities say "Linear: Comment" is Never/);
147 assert.deepEqual(fake.refusals, ["integration:linear:comment=never"]);
148 assert.deepEqual(fake.acted, [], "nothing ran");
149 const jira = await tools.run("act_outside", { reference: "TECH-7", action: "comment", text: "On it." });
150 assert.equal(jira.outcome, "allowed");
151 assert.deepEqual(fake.acted, ["comment TECH-7: On it."]);
152 assert.equal(tools.calls.length, 2, "both calls are in the transcript");
153 assert.equal(tools.calls[0]!.outcome, "refused");
154});
155
156test("reading is alone by default; an item nobody knows is not found, and a system that isn't connected is said so", async () => {
157 const { tools } = await box({ connected: ["linear"] });
158 const read = await tools.run("lookup_outside", { reference: "ENG-42" });
159 assert.equal(read.outcome, "allowed");
160 assert.match(read.text, /<untrusted source="Linear ENG-42">/);
161 assert.match(read.text, /Retry flaky checks/);
162 const missing = await tools.run("lookup_outside", { reference: "NOPE-1" });
163 assert.equal(missing.outcome, "refused");
164 assert.match(missing.text, /No connected integration knows NOPE-1/);
165 // Jira knows TECH-7, but Jira isn't connected to this workspace as the agent sees it.
166 const jira = await tools.run("lookup_outside", { reference: "TECH-7" });
167 assert.equal(jira.outcome, "refused");
168 assert.match(jira.text, /Jira isn't connected to this workspace, so you can't read tickets there/);
169});
170
171// ── Ask first, and alone when asked for it ───────────────────────────────
172
173test("Ask first posts the card with the call, runs nothing, and tells the agent to wait; a session is told it pauses", async () => {
174 const { tools, fake } = await box({ connected: ["linear"], session: true });
175 const tried = await tools.run("act_outside", { reference: "ENG-42", action: "comment", text: "Fixed in #412." });
176 assert.equal(tried.outcome, "refused");
177 assert.match(tried.text, /"Linear: Comment" is after asking first: a card was posted asking @asker \(or an owner\)/);
178 assert.match(tried.text, /Your session pauses/);
179 assert.deepEqual(fake.posted, [{ kind: "ability", title: "Comment on ENG-42 in Linear" }]);
180 assert.deepEqual(fake.acted, []);
181 assert.deepEqual(fake.refusals, ["integration:linear:comment=ask"]);
182});
183
184test("alone when asked for it: runs when the person named the item, asks first when they didn't", async () => {
185 const asked = await box({ connected: ["linear"], said: "Please import ENG-42 into web so we can track it here." });
186 const ran = await asked.tools.run("import_outside", { repo: "web", reference: "ENG-42" });
187 assert.equal(ran.outcome, "allowed");
188 assert.match(ran.text, /Opened acme\/web#7 from ENG-42/);
189 assert.deepEqual(asked.fake.posted, []);
190 const unasked = await box({ connected: ["linear"], said: "What's the state of the queue work?" });
191 const held = await unasked.tools.run("import_outside", { repo: "web", reference: "ENG-42" });
192 assert.equal(held.outcome, "refused");
193 assert.match(held.text, /^Import issues in Linear runs on its own only when asked for it, and this wasn't\. "Linear: Import issues" is only when the person asked for that: a card was posted/);
194 assert.deepEqual(unasked.fake.posted, [{ kind: "ability", title: "Import ENG-42 from Linear into acme/web" }]);
195});
196
197test("when the card can't be posted, the agent is told to ask in words", async () => {
198 const { tools } = await box({ connected: ["linear"], ports: { askFirst: async () => null } });
199 const tried = await tools.run("act_outside", { reference: "ENG-42", action: "comment", text: "x" });
200 assert.match(tried.text, /couldn't be posted just now/);
201});
202
203// ── Whose connection ─────────────────────────────────────────────────────
204
205test("the asker's own connection, missing: a Connect card, nothing runs, and the audit log says why", async () => {
206 const { tools, fake } = await box({ connected: ["linear"], settings: { "integration:linear:read": { credentials: "asker" } } });
207 const tried = await tools.run("lookup_outside", { reference: "ENG-42" });
208 assert.equal(tried.outcome, "refused");
209 assert.match(tried.text, /runs on @asker's own Linear connection, and they haven't connected one. A Connect card was posted/);
210 assert.deepEqual(fake.posted, [{ kind: "connect", title: "Linear" }]);
211 assert.deepEqual(fake.refusals, ["integration:linear:read=asker-not-connected"]);
212 const connected = await box({ connected: ["linear"], settings: { "integration:linear:read": { credentials: "asker" } }, ports: { askerConnected: async () => true } });
213 assert.equal((await connected.tools.run("lookup_outside", { reference: "ENG-42" })).outcome, "allowed");
214});
215
216// ── Requests ─────────────────────────────────────────────────────────────
217
218test("request_ability posts a Request card for an integration that isn't connected, or an ability by its id", async () => {
219 const { tools, fake } = await box({ connected: ["linear"], settings: { "integration:linear:comment": { level: "never" } } });
220 const jira = await tools.run("request_ability", { needs: "jira", why: "Support files bugs there." });
221 assert.equal(jira.outcome, "allowed");
222 const comment = await tools.run("request_ability", { needs: "integration:linear:comment", why: "To tell the team when it's fixed." });
223 assert.equal(comment.outcome, "allowed");
224 assert.deepEqual(fake.posted, [
225 { kind: "request", title: "jira" },
226 { kind: "request", title: "integration:linear:comment" },
227 ]);
228 const bad = await tools.run("request_ability", { needs: "", why: "" });
229 assert.equal(bad.outcome, "refused");
230});
231
232// ── MCP ──────────────────────────────────────────────────────────────────
233
234test("an MCP server's tools are offered as <server>__<tool> with the server's schema; reads run alone, writes ask first, Never withholds", async () => {
235 const { tools, fake } = await box({ servers: [SERVER] });
236 const forecast = tools.definitions().find((t) => t.name === "weather__get_forecast");
237 assert.ok(forecast, "offered");
238 assert.deepEqual(forecast!.input_schema, SERVER.tools[0]!.input_schema);
239 assert.match(forecast!.description, /it reads/);
240 const read = await tools.run("weather__get_forecast", { city: "Lisbon" });
241 assert.equal(read.outcome, "allowed");
242 assert.match(read.text, /<untrusted source="weather get_forecast">\nsunny/);
243 const write = await tools.run("weather__set_alert", {});
244 assert.equal(write.outcome, "refused");
245 assert.match(write.text, /"weather: set_alert" is after asking first/);
246 assert.deepEqual(fake.posted, [{ kind: "ability", title: "Call set_alert on weather" }]);
247 const never = await box({ servers: [SERVER], settings: { "mcp:mcp_w:set_alert": { level: "never" } } });
248 assert.ok(!names(never.tools).includes("weather__set_alert"), "not offered at all");
249 assert.equal((await never.tools.run("weather__set_alert", {})).outcome, "refused");
250});
251
252// ── The prompt and what was said ─────────────────────────────────────────
253
254test("the prompt's abilities section names each connected system's rows and levels, and what isn't connected", async () => {
255 const { sections } = await box({ connected: ["linear"], settings: { "integration:linear:comment": { level: "never" } }, servers: [SERVER] });
256 const text = abilitiesSection(sections)!;
257 assert.match(text, /- Linear: read issues on your own; import issues on your own only when they asked for it, else it asks first; comment never\./);
258 assert.match(text, /- weather: get_forecast on your own; set_alert asks first \(a card\)\./);
259 assert.match(text, /Not connected to this workspace: Jira, Sentry\./);
260 assert.equal(saidText([null, " ", "a", "b"]), "a\nb");
261});
262
263// ── The definition ───────────────────────────────────────────────────────
264
265test("a definition keeps ability settings it knows, within each kind's limit, and never above Ask for a restricted one", () => {
266 const base = applyChanges(null, { handle: "margo", display_name: "Margo", title: "QA", instructions: "Test." }, []);
267 assert.ok(base.ok);
268 const changed = applyChanges(base.value, { abilities: { settings: { "integration:linear:comment": { level: "alone", credentials: "asker" } } } }, []);
269 assert.ok(changed.ok);
270 assert.deepEqual(changed.value.abilities, { settings: { "integration:linear:comment": { level: "alone", credentials: "asker" } }, mcp_servers: [] });
271 const unknown = applyChanges(base.value, { abilities: { settings: { "integration:slack:post": { level: "alone" } } } }, []);
272 assert.ok(!unknown.ok && /no ability called integration:slack:post/.test(unknown.message));
273 const badLevel = applyChanges(base.value, { abilities: { settings: { "integration:linear:read": { level: "loud" as never } } } }, []);
274 assert.ok(!badLevel.ok);
275 const notIntegration = applyChanges(base.value, { abilities: { settings: { "mcp:mcp_w:get_forecast": { level: "ask" } } } }, []);
276 assert.ok(!notIntegration.ok, "no such server on the agent");
277 // With the server, its tools are abilities; removing the server drops their settings.
278 const withServer = applyChanges(base.value, { abilities: { settings: { "mcp:mcp_w:get_forecast": { level: "ask" } } } }, [], { mcp_servers: [SERVER] });
279 assert.ok(withServer.ok);
280 assert.equal(withServer.value.abilities.mcp_servers.length, 1);
281 assert.deepEqual(withServer.value.abilities.settings, { "mcp:mcp_w:get_forecast": { level: "ask" } });
282 const without = applyChanges(withServer.value, {}, [], { mcp_servers: [] });
283 assert.ok(without.ok);
284 assert.deepEqual(without.value.abilities, { settings: {}, mcp_servers: [] });
285 const g1t = applyChanges(base.value, { abilities: { settings: { "g1t:merge": { level: "alone" } } } }, []);
286 assert.ok(!g1t.ok, "g1t's own keep their choices in autonomy");
287});
288
289// ── Cards ────────────────────────────────────────────────────────────────
290
291test("the Ask-first, Connect and Request cards say what they are for and offer the right buttons", () => {
292 const waiting = abilityCard({ id: "abr_1", summary: "Comment on ENG-42 in Linear", status: "pending", decided_by: null, result: null, ability: "integration:linear:comment" }, { agent: "Margo", asker: "ana", rule: "Linear: Comment", level: "ask", note: null, body: "Fixed in #412." });
293 assert.equal(waiting.state, "Waiting");
294 assert.deepEqual(waiting.actions?.map((a) => a.id), ["allow", "deny"]);
295 assert.equal(waiting.owner, "agents");
296 assert.equal(waiting.body, "Fixed in #412.");
297 const done = abilityCard({ id: "abr_1", summary: "Comment on ENG-42 in Linear", status: "allowed", decided_by: "ana", result: "Commented on ENG-42.", ability: "integration:linear:comment" }, { agent: "Margo", asker: "ana", rule: "Linear: Comment", level: "ask", note: null, body: null });
298 assert.equal(done.state, "Done");
299 assert.equal(done.actions, undefined);
300 const connect = connectCard({ connector: "Linear", agent: "Margo", ability: "Read issues", asker: "ana", href: "/settings/integrations" });
301 assert.equal(connect.actions?.[0]?.href, "/settings/integrations");
302 assert.equal(connect.owner, null, "a link, no action for the service");
303 const request = requestCard({ agent: { id: "a1", handle: "margo", display_name: "Margo" }, workspace: "acme", connector: { id: "jira", name: "Jira", available: true }, ability: null, why: "Support files bugs there.", status: "open", by: null });
304 assert.deepEqual(request.actions?.map((a) => a.id), ["ask", "open"]);
305 assert.equal(request.ref, "connector:a1:jira");
306 const asked = requestCard({ agent: { id: "a1", handle: "margo", display_name: "Margo" }, workspace: "acme", connector: null, ability: { id: "integration:linear:comment", label: "Linear: Comment" }, why: "x", status: "asked", by: "bo" });
307 assert.equal(asked.state, "Asked");
308 assert.equal(asked.actions?.[0]?.href, "/acme/-/agents/margo/abilities");
309});
310
311// ── The MCP client ───────────────────────────────────────────────────────
312
313test("the MCP client lists tools (reads by readOnlyHint) and calls one, over JSON or an event stream", async () => {
314 const seen: { method: string; session: string | null }[] = [];
315 const fetchFn = async (_url: string, init: RequestInit) => {
316 const body = JSON.parse(String(init.body)) as { id?: number; method: string; params?: { name?: string } };
317 seen.push({ method: body.method, session: (init.headers as Record<string, string>)["mcp-session-id"] ?? null });
318 const answer = (result: unknown, sse = false) =>
319 new Response(sse ? `event: message\ndata: ${JSON.stringify({ jsonrpc: "2.0", id: body.id, result })}\n\n` : JSON.stringify({ jsonrpc: "2.0", id: body.id, result }), {
320 status: 200,
321 headers: { "content-type": sse ? "text/event-stream" : "application/json", "mcp-session-id": "s1" },
322 });
323 if (body.method === "initialize") return answer({ protocolVersion: "2025-06-18" });
324 if (body.method === "notifications/initialized") return new Response(null, { status: 202 });
325 if (body.method === "tools/list") {
326 return answer(
327 {
328 tools: [
329 { name: "get_forecast", description: "Forecast", inputSchema: { type: "object", properties: { city: { type: "string" } } }, annotations: { readOnlyHint: true } },
330 { name: "set_alert", description: "Alert" },
331 { name: "bad name!" },
332 ],
333 },
334 true,
335 );
336 }
337 if (body.method === "tools/call") return answer({ content: [{ type: "text", text: `sunny in ${JSON.stringify(body.params)}` }] });
338 return new Response("{}", { status: 404 });
339 };
340 const listed = await listMcpTools("https://mcp.example.com/", fetchFn);
341 assert.ok(listed.ok);
342 assert.deepEqual(
343 listed.tools.map((t) => [t.name, t.kind]),
344 [
345 ["get_forecast", "read"],
346 ["set_alert", "write"],
347 ],
348 );
349 assert.deepEqual(listed.tools[1]!.input_schema, { type: "object", properties: {} });
350 assert.equal(seen[2]!.session, "s1", "the session id the server gave rides along");
351 const called = await callMcpTool("https://mcp.example.com/", "get_forecast", { city: "Lisbon" }, fetchFn);
352 assert.ok(called.ok);
353 assert.match(called.text, /sunny in \{"name":"get_forecast","arguments":\{"city":"Lisbon"\}\}/);
354 const down = await listMcpTools("https://mcp.example.com/", async () => {
355 throw new Error("ECONNREFUSED");
356 });
357 assert.ok(!down.ok && /couldn't be reached/.test(down.message));
358 const errored = await callMcpTool("https://mcp.example.com/", "x", {}, async (_u, init) => {
359 const body = JSON.parse(String(init.body)) as { id?: number; method: string };
360 if (body.method === "tools/call") return Response.json({ jsonrpc: "2.0", id: body.id, result: { isError: true, content: [{ type: "text", text: "no such city" }] } });
361 return Response.json({ jsonrpc: "2.0", id: body.id, result: {} });
362 });
363 assert.ok(!errored.ok && errored.message === "no such city");
364});