diff --git a/e2e/scenarios/toolkits-mcp.test.ts b/e2e/scenarios/toolkits-mcp.test.ts index bd0b91229a..b84eed1e15 100644 --- a/e2e/scenarios/toolkits-mcp.test.ts +++ b/e2e/scenarios/toolkits-mcp.test.ts @@ -584,6 +584,73 @@ scenario( }), ); +scenario( + "Toolkits · a broad approve policy applies over a narrower connection grant", + { timeout: 240_000 }, + Effect.gen(function* () { + const target = yield* Target; + const mcp = yield* Mcp; + const { client: makeClient } = yield* Api; + const identity = yield* target.newIdentity(); + const client = yield* makeClient(api, identity); + + const toolkitName = unique("broad-approve-kit"); + const createdPattern = `${unique("broad-approved-policy")}.*`; + + yield* Effect.gen(function* () { + const toolkit = yield* client.toolkits.create({ + payload: { owner: "org", name: toolkitName }, + }); + yield* client.toolkits.createConnection({ + params: { toolkitId: toolkit.id }, + payload: { pattern: "executor.coreTools.policies.create" }, + }); + yield* client.toolkits.createPolicy({ + params: { toolkitId: toolkit.id }, + payload: { pattern: "executor.coreTools.*", action: "approve" }, + }); + + const session = mcp.session(identity, { + url: toolkitUrl(target.baseUrl, toolkit.slug), + }); + const result = yield* session.call("execute", { + code: createPolicyCode({ pattern: createdPattern, action: "block" }), + }); + + expect(result.text, "the broad approve policy does not pause execution").not.toContain( + "Execution paused", + ); + expect(result.ok, `the approved tool succeeds: ${result.text}`).toBe(true); + const policies = yield* client.policies.list(); + expect( + policies.map((policy) => `${policy.owner} ${policy.pattern} ${policy.action}`), + "the approved tool reaches its side effect", + ).toContain(`user ${createdPattern} block`); + }).pipe( + Effect.ensuring( + Effect.gen(function* () { + const listed = yield* client.toolkits.list(); + yield* Effect.forEach( + listed.toolkits.filter((toolkit) => toolkit.name === toolkitName), + (toolkit) => client.toolkits.remove({ params: { toolkitId: toolkit.id } }), + { discard: true }, + ); + const policies = yield* client.policies.list(); + yield* Effect.forEach( + policies.filter((policy) => policy.pattern === createdPattern), + (policy) => + client.policies.remove({ + params: { policyId: policy.id }, + payload: { owner: policy.owner }, + }), + { discard: true }, + ); + }).pipe(Effect.ignore), + ), + ); + }), +); + scenario( "Toolkits · the provider catalog hides integrations the toolkit grants no tools", { timeout: 240_000 }, diff --git a/packages/plugins/toolkits/src/server.test.ts b/packages/plugins/toolkits/src/server.test.ts index 53d0d53cbd..bab67eb0e9 100644 --- a/packages/plugins/toolkits/src/server.test.ts +++ b/packages/plugins/toolkits/src/server.test.ts @@ -165,4 +165,38 @@ describe("toolkitsPlugin", () => { ).toContain("executor.coreTools.* approve"); }), ); + + it.effect("applies a broad approve policy over a narrower connection", () => + Effect.gen(function* () { + const executor = yield* makeTestExecutor({ + plugins: [toolkitsPlugin()] as const, + }); + + const toolkit = yield* executor.toolkits.create({ + owner: "org", + name: "Docs Kit", + }); + yield* executor.toolkits.createConnection(toolkit.id, { + pattern: "google_docs.org.main.*", + }); + yield* executor.toolkits.createPolicy(toolkit.id, { + pattern: "google_docs.org.*", + action: "approve", + }); + + const result = yield* executor.toolkits.resolvePolicyForSlug( + toolkit.slug, + "google_docs.org.main.documents.update", + true, + ); + expect(result.action).toBe("approve"); + expect(result.source).toBe("user"); + + const rules = yield* executor.toolkits.policyRulesForSlug(toolkit.slug); + expect( + rules.map((rule) => `${rule.pattern} ${rule.action}`), + "policy listing agrees with toolkit enforcement", + ).toContain("google_docs.org.* approve"); + }), + ); }); diff --git a/packages/plugins/toolkits/src/server.ts b/packages/plugins/toolkits/src/server.ts index 0b70e33575..7dacc3e456 100644 --- a/packages/plugins/toolkits/src/server.ts +++ b/packages/plugins/toolkits/src/server.ts @@ -181,11 +181,12 @@ const legacyConnectionPolicyIds = ( policies: readonly ToolkitPolicyRecord[], connections: readonly ToolkitConnectionRecord[], ): ReadonlySet => { - const connectionPatterns = new Set(connections.map((connection) => connection.pattern)); return new Set( policies .filter( - (policy) => isLegacyConnectionPolicy(policy) && !connectionPatterns.has(policy.pattern), + (policy) => + isLegacyConnectionPolicy(policy) && + !connections.some((connection) => matchPattern(policy.pattern, connection.pattern)), ) .map((policy) => policy.id), );