Q104: connector grants can narrow a toolkit to named actions and fixed argument values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b02834893f
commit
558d678ba2
14 files changed
+202
-26
No files matched your search
@@ -158,7 +158,7 @@ describe("agent instruction wiring", () => {
|
||||
values ('ca_github', ?, 'github', 'GitHub', 'main', 'ac_github', 'ACTIVE', ?, ?)
|
||||
`).run(orgId, now, now);
|
||||
new ConnectorGrantsService(connection.db).grant({
|
||||
orgId, agentId, toolkitSlug: "github", accountLabel: null, grantedByType: "employee", grantedById: null,
|
||||
orgId, agentId, toolkitSlug: "github", accountLabel: null, limits: { actions: null, fixedArguments: null }, grantedByType: "employee", grantedById: null,
|
||||
});
|
||||
taskId = organization.createTask({
|
||||
orgId, title: "Ship it", description: "d", status: TASK_STATUS.IN_PROGRESS,
|
||||
|
||||
@@ -58,7 +58,7 @@ async function connectedCompany() {
|
||||
const first = await service.connectToolkit(orgId, "github", "main");
|
||||
remote.get(first.accountId)!.status = "ACTIVE";
|
||||
await service.refreshAccounts(orgId);
|
||||
new ConnectorGrantsService(connection.db).grant({ orgId, agentId: ceoId, toolkitSlug: "github", accountLabel: null, grantedByType: "agent", grantedById: ceoId });
|
||||
new ConnectorGrantsService(connection.db).grant({ orgId, agentId: ceoId, toolkitSlug: "github", accountLabel: null, limits: { actions: null, fixedArguments: null }, grantedByType: "agent", grantedById: ceoId });
|
||||
const account = () => connection.db.prepare("select id, status, pending_account_id as pending from connector_accounts where org_id = ?").get(orgId) as { id: string; status: string; pending: string | null };
|
||||
const grants = () => (connection.db.prepare("select count(*) as n from agent_connector_grants where org_id = ?").get(orgId) as { n: number }).n;
|
||||
return { connection, orgId, service, oldId: first.accountId, account, grants };
|
||||
|
||||
@@ -175,13 +175,42 @@ describe("agents reach every action through search and run", () => {
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a grant limited to some actions and one resource shows and runs only those", async () => {
|
||||
const withRepo = (slug: string, description: string) => ({ slug, name: slug, description, input_parameters: { type: "object", properties: { repo: { type: "string" } } }, toolkit: { slug: "github" } });
|
||||
pages.set("github:", { items: [withRepo("GITHUB_LIST_ISSUES", "list issues"), withRepo("GITHUB_CREATE_ISSUE", "create issue"), tool("GITHUB_LIST_ALL_REPOS")], next_cursor: null });
|
||||
const h = company("ak_limits");
|
||||
const staff = h.connection.transaction(() => {
|
||||
const providerId = (h.connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(h.orgId) as { id: string }).id;
|
||||
const id = new OrganizationService(h.connection.db).createAgent({ orgId: h.orgId, name: "Ada", role: "Engineer", department: "Engineering", modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 1000, managerAgentId: h.ceoId });
|
||||
new ConnectorGrantsService(h.connection.db).grant({ orgId: h.orgId, agentId: id, toolkitSlug: "github", accountLabel: null, limits: { actions: ["github_list_issues", "GITHUB_LIST_ALL_REPOS"], fixedArguments: { repo: "site" } }, grantedByType: "agent", grantedById: h.ceoId });
|
||||
return id;
|
||||
});
|
||||
const asAda = (name: string, args: Record<string, unknown>) => executeComposioTool(h.connection.db, h.keys, h.orgId, staff, { id: "c9", name, arguments: args });
|
||||
const found = JSON.parse(await asAda("composio_find_actions", { query: "issue" })) as { matches: Array<{ action: string }> };
|
||||
expect(found.matches.map((match) => match.action)).toEqual(["GITHUB_LIST_ISSUES"]);
|
||||
expect(await asAda("composio_run_action", { action: "GITHUB_CREATE_ISSUE", arguments: { repo: "site" } }))
|
||||
.toBe("ERROR: Your grant for github allows only GITHUB_LIST_ISSUES, GITHUB_LIST_ALL_REPOS; GITHUB_CREATE_ISSUE is not among them.");
|
||||
expect(await asAda("composio_run_action", { action: "GITHUB_LIST_ISSUES", arguments: { repo: "secret" } }))
|
||||
.toBe('ERROR: Your grant for github is limited to repo="site"; this call has repo="secret".');
|
||||
expect(await asAda("composio_run_action", { action: "GITHUB_LIST_ISSUES", arguments: {} }))
|
||||
.toBe('ERROR: Your grant for github is limited to repo="site"; pass repo with that value.');
|
||||
expect(await asAda("composio_run_action", { action: "GITHUB_LIST_ALL_REPOS", arguments: {} }))
|
||||
.toBe('ERROR: Your grant for github is limited to repo="site", and GITHUB_LIST_ALL_REPOS takes no repo.');
|
||||
const sent = written.length;
|
||||
expect(await asAda("composio_run_action", { action: "GITHUB_LIST_ISSUES", arguments: { repo: "site" } })).toBe(JSON.stringify({ number: 42 }));
|
||||
expect(written.length).toBe(sent + 1);
|
||||
// The CEO holds every action by role.
|
||||
expect(await h.call("composio_run_action", { action: "GITHUB_CREATE_ISSUE", arguments: { repo: "other" } })).toBe(JSON.stringify({ number: 42 }));
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a grant revoked while the catalog loads stops the action, and a search is not journaled as work", async () => {
|
||||
pages.set("github:", { items: [tool("GITHUB_STAR_A_REPOSITORY")], next_cursor: null });
|
||||
const h = company("ak_revoke");
|
||||
const staff = h.connection.transaction(() => {
|
||||
const providerId = (h.connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(h.orgId) as { id: string }).id;
|
||||
const id = new OrganizationService(h.connection.db).createAgent({ orgId: h.orgId, name: "Ada", role: "Engineer", department: "Engineering", modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 1000, managerAgentId: h.ceoId });
|
||||
new ConnectorGrantsService(h.connection.db).grant({ orgId: h.orgId, agentId: id, toolkitSlug: "github", accountLabel: null, grantedByType: "agent", grantedById: h.ceoId });
|
||||
new ConnectorGrantsService(h.connection.db).grant({ orgId: h.orgId, agentId: id, toolkitSlug: "github", accountLabel: null, limits: { actions: null, fixedArguments: null }, grantedByType: "agent", grantedById: h.ceoId });
|
||||
return id;
|
||||
});
|
||||
const running = executeComposioTool(h.connection.db, h.keys, h.orgId, staff, { id: "c2", name: "composio_run_action", arguments: { action: "GITHUB_STAR_A_REPOSITORY", arguments: {} } });
|
||||
|
||||
@@ -5,7 +5,7 @@ import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { ChatService } from "../../src/main/services/chat-service";
|
||||
import { ConnectorGrantsService } from "../../src/main/services/composio/connector-grants";
|
||||
import { ConnectorGrantsService, grantLimitRefusal } from "../../src/main/services/composio/connector-grants";
|
||||
import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools";
|
||||
|
||||
function call(name: string, args: Record<string, unknown>) {
|
||||
@@ -71,7 +71,7 @@ describe("connector grants", () => {
|
||||
const h = harness();
|
||||
const grants = new ConnectorGrantsService(h.connection.db);
|
||||
expect(() => grants.grant({
|
||||
orgId: h.orgId, agentId: h.danaId, toolkitSlug: "github", accountLabel: null, grantedByType: "employee", grantedById: null,
|
||||
orgId: h.orgId, agentId: h.danaId, toolkitSlug: "github", accountLabel: null, limits: { actions: null, fixedArguments: null }, grantedByType: "employee", grantedById: null,
|
||||
})).toThrow(/No active connection/);
|
||||
});
|
||||
|
||||
@@ -122,6 +122,29 @@ describe("connector grants", () => {
|
||||
expect(grants.toolkitsForAgent(h.orgId, h.ceoId)).toEqual(["github"]);
|
||||
});
|
||||
|
||||
test("the CEO grants a toolkit narrowed to some actions and argument values", () => {
|
||||
const h = harness();
|
||||
connectToolkit(h, "gmail", "Gmail");
|
||||
expect(dispatchAgentTool(h.deps, h.ceoCtx, call("grant_connector", { agent_name: "Dana", toolkit: "gmail", actions: ["gmail_fetch_emails"], fixed_arguments: { label: "support" } })))
|
||||
.toContain('through its "main" account (only GMAIL_FETCH_EMAILS; limited to {"label":"support"})');
|
||||
expect(dispatchAgentTool(h.deps, h.danaCtx, call("list_connectors", {})))
|
||||
.toContain('Gmail (gmail), account "main": granted to Dana (only GMAIL_FETCH_EMAILS; limited to {"label":"support"})');
|
||||
expect(new ConnectorGrantsService(h.connection.db).limitsForAgent(h.orgId, h.danaId, "gmail")).toEqual({ actions: ["GMAIL_FETCH_EMAILS"], fixedArguments: { label: "support" } });
|
||||
expect(dispatchAgentTool(h.deps, h.ceoCtx, call("grant_connector", { agent_name: "Dana", toolkit: "gmail", actions: [] })))
|
||||
.toBe("ERROR: actions must name at least one action, none blank.");
|
||||
expect(dispatchAgentTool(h.deps, h.ceoCtx, call("grant_connector", { agent_name: "Dana", toolkit: "gmail", fixed_arguments: ["label"] })))
|
||||
.toBe('ERROR: "fixed_arguments" must be an object of argument values.');
|
||||
const nullRepo = { actions: null, fixedArguments: { repo: null } };
|
||||
const action = { name: "GITHUB_LIST_ISSUES", parameters: { properties: { repo: {} } } };
|
||||
expect(grantLimitRefusal(nullRepo, "github", action, {})).toBe("Your grant for github is limited to repo=null; pass repo with that value.");
|
||||
expect(grantLimitRefusal(nullRepo, "github", action, { repo: null })).toBeNull();
|
||||
const proto = { actions: null, fixedArguments: JSON.parse('{"__proto__": {}}') as Record<string, unknown> };
|
||||
expect(grantLimitRefusal(proto, "github", action, {})).toBe("Your grant for github is limited to __proto__={}, and GITHUB_LIST_ISSUES takes no __proto__.");
|
||||
// A grant without limits replaces a limited one.
|
||||
dispatchAgentTool(h.deps, h.ceoCtx, call("grant_connector", { agent_name: "Dana", toolkit: "gmail" }));
|
||||
expect(new ConnectorGrantsService(h.connection.db).limitsForAgent(h.orgId, h.danaId, "gmail")).toEqual({ actions: null, fixedArguments: null });
|
||||
});
|
||||
|
||||
test("staff cannot hand out connector access", () => {
|
||||
const h = harness();
|
||||
connectToolkit(h, "github", "GitHub");
|
||||
@@ -134,7 +157,7 @@ describe("connector grants", () => {
|
||||
const h = harness();
|
||||
connectToolkit(h, "slack", "Slack");
|
||||
const grants = new ConnectorGrantsService(h.connection.db);
|
||||
grants.grant({ orgId: h.orgId, agentId: h.danaId, toolkitSlug: "slack", accountLabel: null, grantedByType: "employee", grantedById: null });
|
||||
grants.grant({ orgId: h.orgId, agentId: h.danaId, toolkitSlug: "slack", accountLabel: null, limits: { actions: null, fixedArguments: null }, grantedByType: "employee", grantedById: null });
|
||||
expect(grants.toolkitsForAgent(h.orgId, h.danaId)).toEqual(["slack"]);
|
||||
h.connection.db.prepare("update connector_accounts set status = 'EXPIRED' where org_id = ? and toolkit_slug = 'slack'").run(h.orgId);
|
||||
expect(grants.toolkitsForAgent(h.orgId, h.danaId)).toEqual([]);
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user