History

maybe: mcp elicitation + openapi approvals

Rhys Sullivan committed 7 months ago

Commit 38d84c81 parent 8ece7d217 files · +192 −106Browse files

Unified · Split

17 files
  1. apps/server/src/handlers/tools.ts +0 −6
  2. apps/web/vite.config.ts +59 −39
  3. packages/clients/react/src/atoms.ts +1 −1
  4. packages/clients/react/src/index.ts +0 −1
  5. packages/core/api/src/tools/api.ts +0 −31
  6. packages/core/execution/src/engine.ts +3 −3
  7. packages/core/execution/src/tool-invoker.ts +2 −2
  8. packages/core/sdk/src/executor.ts +34 −3
  9. packages/core/sdk/src/in-memory/tool-registry.ts +10 −1
  10. packages/core/sdk/src/index.test.ts +10 −7
  11. packages/core/sdk/src/index.ts +1 −0
  12. packages/core/sdk/src/plugins/in-memory-tools.ts +7 −2
  13. packages/core/sdk/src/tools.ts +23 −4
  14. packages/core/storage-file/src/tool-registry.ts +10 −1
  15. packages/plugins/openapi/src/sdk/invoke.ts +24 −1
  16. packages/plugins/openapi/src/sdk/plugin.test.ts +7 −4
  17. packages/plugins/openapi/src/sdk/plugin.ts +1 −0
apps/server/src/handlers/tools.ts+0−6
@@ -28,11 +28,5 @@ export const ToolsHandlers = HttpApiBuilder.group( · 27 unmodified lines
2828 const executor = yield* ExecutorService;
2929 return yield* executor.tools.schema(path.toolId);
3030 }),
31− )
32− .handle("invoke", ({ path, payload }) =>
33− Effect.gen(function* () {
34− const executor = yield* ExecutorService;
35− return yield* executor.tools.invoke(path.toolId, payload.args);
36− }),
3731 ),
3832 );
apps/web/vite.config.ts+59−39
@@ -5,6 +5,63 @@ import { createServerHandlers } from "@executor/server"; · 4 unmodified lines
55
66 const handlersPromise = createServerHandlers();
77
8+// Build a Web Request from a Node IncomingMessage
9+const toWebRequest = async (
10+ req: import("http").IncomingMessage,
11+): Promise<Request> => {
12+ const headers = new Headers();
13+ for (const [key, value] of Object.entries(req.headers)) {
14+ if (value === undefined) continue;
15+ if (Array.isArray(value)) {
16+ for (const v of value) headers.append(key, v);
17+ } else {
18+ headers.set(key, value);
19+ }
20+ }
21+
22+ const host = req.headers.host ?? "localhost";
23+ const url = `http://${host}${req.url}`;
24+
25+ return new Request(url, {
26+ method: req.method,
27+ headers,
28+ body:
29+ req.method !== "GET" && req.method !== "HEAD"
30+ ? (await new Promise<Buffer>((resolve) => {
31+ const chunks: Buffer[] = [];
32+ req.on("data", (c: Buffer) => chunks.push(c));
33+ req.on("end", () => resolve(Buffer.concat(chunks)));
34+ }) as unknown as BodyInit)
35+ : undefined,
36+ duplex: "half" as const,
37+ });
38+};
39+
40+// Pipe a Web Response back to a Node ServerResponse, streaming if needed
41+const sendWebResponse = async (
42+ webRes: Response,
43+ nodeRes: import("http").ServerResponse,
44+) => {
45+ nodeRes.statusCode = webRes.status;
46+ webRes.headers.forEach((value, key) => nodeRes.setHeader(key, value));
47+
48+ if (!webRes.body) {
49+ nodeRes.end();
50+ return;
51+ }
52+
53+ const reader = webRes.body.getReader();
54+ try {
55+ for (;;) {
56+ const { done, value } = await reader.read();
57+ if (done) break;
58+ nodeRes.write(value);
59+ }
60+ } finally {
61+ nodeRes.end();
62+ }
63+};
64+
865 export default defineConfig({
966 plugins: [
1067 react(),
@@ -24,50 +81,13 @@ export default defineConfig({ · 13 unmodified lines
2481 if (!isApi && !isMcp) return next();
2582
2683 const handlers = await handlersPromise;
27−
28− // Build a standard Request from the Node HTTP request
29− const headers = new Headers();
30− for (const [key, value] of Object.entries(req.headers)) {
31− if (value === undefined) continue;
32− if (Array.isArray(value)) {
33− for (const v of value) headers.append(key, v);
34− } else {
35− headers.set(key, value);
36− }
37− }
38−
39− const host = req.headers.host ?? "localhost";
40− const request = new Request(`http://${host}${url}`, {
41− method: req.method,
42− headers,
43− body:
44− req.method !== "GET" && req.method !== "HEAD"
45− ? (await new Promise<Uint8Array>((resolve) => {
46− const chunks: Uint8Array[] = [];
47− req.on("data", (c: Uint8Array) => chunks.push(c));
48− req.on("end", () => {
49− const total = chunks.reduce((n, c) => n + c.length, 0);
50− const buf = new Uint8Array(total);
51− let offset = 0;
52− for (const c of chunks) {
53− buf.set(c, offset);
54− offset += c.length;
55− }
56− resolve(buf);
57− });
58− }) as unknown as BodyInit)
59− : undefined,
60− });
84+ const request = await toWebRequest(req);
6185
6286 const response = isMcp
6387 ? await handlers.mcp.handleRequest(request)
6488 : await handlers.api.handler(request);
6589
66− res.statusCode = response.status;
67− response.headers.forEach((value, key) => res.setHeader(key, value));
68−
69− const body = await response.arrayBuffer();
70− res.end(Buffer.from(body));
90+ await sendWebResponse(response, res);
7191 });
7292 },
7393 },
packages/clients/react/src/atoms.ts+1−1
@@ -58,7 +58,7 @@ export const secretStatusAtom = (scopeId: ScopeId, secretId: SecretId) => · 57 unmodified lines
5858 // Mutation atoms
5959 // ---------------------------------------------------------------------------
6060
61−export const invokeTool = ExecutorApiClient.mutation("tools", "invoke");
61+
6262
6363 export const setSecret = ExecutorApiClient.mutation("secrets", "set");
6464
packages/clients/react/src/index.ts+0−1
@@ -28,7 +28,6 @@ export { · 27 unmodified lines
2828 sourceAtom,
2929 secretsAtom,
3030 secretStatusAtom,
31− invokeTool,
3231 setSecret,
3332 resolveSecret,
3433 removeSecret,
packages/core/api/src/tools/api.ts+0−31
@@ -4,9 +4,6 @@ import { · 3 unmodified lines
44 ScopeId,
55 ToolId,
66 ToolNotFoundError,
7− ToolInvocationError,
8− PolicyDeniedError,
9− ElicitationDeclinedError,
107 } from "@executor/sdk";
118
129 // ---------------------------------------------------------------------------
@@ -35,16 +32,6 @@ const ToolSchemaResponse = Schema.Struct({ · 22 unmodified lines
3532 outputSchema: Schema.optional(Schema.Unknown),
3633 });
3734
38−const ToolInvokePayload = Schema.Struct({
39− args: Schema.Unknown,
40−});
41−
42−const ToolInvokeResponse = Schema.Struct({
43− data: Schema.Unknown,
44− error: Schema.NullOr(Schema.Unknown),
45− status: Schema.optional(Schema.Number),
46−});
47−
4835 // ---------------------------------------------------------------------------
4936 // Error schemas with HTTP status annotations
5037 // ---------------------------------------------------------------------------
@@ -52,15 +39,6 @@ const ToolInvokeResponse = Schema.Struct({ · 1 unmodified line
5239 const ToolNotFound = ToolNotFoundError.annotations(
5340 HttpApiSchema.annotations({ status: 404 }),
5441 );
55−const ToolInvocation = ToolInvocationError.annotations(
56− HttpApiSchema.annotations({ status: 500 }),
57−);
58−const PolicyDenied = PolicyDeniedError.annotations(
59− HttpApiSchema.annotations({ status: 403 }),
60−);
61−const ElicitationDeclined = ElicitationDeclinedError.annotations(
62− HttpApiSchema.annotations({ status: 422 }),
63−);
6442
6543 // ---------------------------------------------------------------------------
6644 // Group
@@ -76,13 +54,4 @@ export class ToolsApi extends HttpApiGroup.make("tools") · 9 unmodified lines
7654 .addSuccess(ToolSchemaResponse)
7755 .addError(ToolNotFound),
7856 )
79− .add(
80− HttpApiEndpoint.post("invoke")`/scopes/${scopeIdParam}/tools/${toolIdParam}/invoke`
81− .setPayload(ToolInvokePayload)
82− .addSuccess(ToolInvokeResponse)
83− .addError(ToolNotFound)
84− .addError(ToolInvocation)
85− .addError(PolicyDenied)
86− .addError(ElicitationDeclined),
87− )
8857 .prefix("/v1") {}
packages/core/execution/src/engine.ts+3−3
@@ -125,7 +125,7 @@ export const formatPausedExecution = (paused: PausedExecution): { · 124 unmodified lines
125125
126126 const makeFullInvoker = (
127127 executor: Executor,
128− invokeOptions?: InvokeOptions,
128+ invokeOptions: InvokeOptions,
129129 ): SandboxToolInvoker => {
130130 const base = makeExecutorToolInvoker(executor, { invokeOptions });
131131 return {
@@ -155,7 +155,7 @@ export type ExecutionEngine = { · 23 unmodified lines
155155 */
156156 readonly execute: (
157157 code: string,
158− options?: { readonly onElicitation?: ElicitationHandler },
158+ options: { readonly onElicitation: ElicitationHandler },
159159 ) => Promise<ExecuteResult>;
160160
161161 /**
@@ -188,7 +188,7 @@ export const createExecutionEngine = (config: ExecutionEngineConfig): ExecutionE · 26 unmodified lines
188188 return {
189189 execute: async (code, options) => {
190190 const invoker = makeFullInvoker(executor, {
191− onElicitation: options?.onElicitation,
191+ onElicitation: options.onElicitation,
192192 });
193193 return runEffect(codeExecutor.execute(code, invoker));
194194 },
packages/core/execution/src/tool-invoker.ts+2−2
@@ -8,14 +8,14 @@ import type { SandboxToolInvoker } from "@executor/codemode-core"; · 7 unmodified lines
88 */
99 export const makeExecutorToolInvoker = (
1010 executor: Executor,
11− options?: { readonly invokeOptions?: InvokeOptions },
11+ options: { readonly invokeOptions: InvokeOptions },
1212 ): SandboxToolInvoker => ({
1313 invoke: ({ path, args }) =>
1414 Effect.gen(function* () {
1515 const result = yield* executor.tools.invoke(
1616 path as ToolId,
1717 args,
18− options?.invokeOptions,
18+ options.invokeOptions,
1919 );
2020 if (result.error !== null && result.error !== undefined) {
2121 return yield* Effect.fail(result.error);
packages/core/sdk/src/executor.ts+34−3
@@ -25,7 +25,17 @@ import type { · 24 unmodified lines
2525 SecretResolutionError,
2626 PolicyDeniedError,
2727 } from "./errors";
28−import type { ElicitationDeclinedError } from "./elicitation";
28+import {
29+ FormElicitation,
30+ ElicitationDeclinedError,
31+ ElicitationResponse,
32+ type ElicitationHandler,
33+} from "./elicitation";
34+
35+const resolveElicitationHandler = (options: InvokeOptions): ElicitationHandler =>
36+ options.onElicitation === "accept-all"
37+ ? () => Effect.succeed(new ElicitationResponse({ action: "accept" }))
38+ : options.onElicitation;
2939
3040 // ---------------------------------------------------------------------------
3141 // Executor — the main public API, expands with plugins
@@ -46,7 +56,7 @@ export type Executor< · 14 unmodified lines
4656 readonly invoke: (
4757 toolId: string,
4858 args: unknown,
49− options?: InvokeOptions,
59+ options: InvokeOptions,
5060 ) => Effect.Effect<
5161 ToolInvocationResult,
5262 | ToolNotFoundError
@@ -149,10 +159,31 @@ export const createExecutor = < · 96 unmodified lines
149159 list: (filter?: ToolListFilter) => tools.list(filter),
150160 schema: (toolId: string) => tools.schema(toolId as ToolId),
151161 definitions: () => tools.definitions(),
152− invoke: (toolId: string, args: unknown, options?: InvokeOptions) => {
162+ invoke: (toolId: string, args: unknown, options: InvokeOptions) => {
153163 const tid = toolId as ToolId;
154164 return Effect.gen(function* () {
155165 yield* policies.check({ scopeId: scope.id, toolId: tid });
166+
167+ // Dynamically resolve annotations from the plugin
168+ const annotations = yield* tools.resolveAnnotations(tid);
169+ if (annotations?.requiresApproval) {
170+ const handler = resolveElicitationHandler(options);
171+ const response = yield* handler({
172+ toolId: tid,
173+ args,
174+ request: new FormElicitation({
175+ message: annotations.approvalDescription ?? `Approve ${toolId}?`,
176+ requestedSchema: {},
177+ }),
178+ });
179+ if (response.action !== "accept") {
180+ return yield* new ElicitationDeclinedError({
181+ toolId: tid,
182+ action: response.action,
183+ });
184+ }
185+ }
186+
156187 return yield* tools.invoke(tid, args, options);
157188 });
158189 },
packages/core/sdk/src/in-memory/tool-registry.ts+10−1
@@ -66,7 +66,16 @@ export const makeInMemoryToolRegistry = () => { · 65 unmodified lines
6666 invokers.set(pluginKey, invoker);
6767 }),
6868
69− invoke: (toolId: ToolId, args: unknown, options?: InvokeOptions) =>
69+ resolveAnnotations: (toolId: ToolId) =>
70+ Effect.gen(function* () {
71+ const tool = tools.get(toolId);
72+ if (!tool) return undefined;
73+ const invoker = invokers.get(tool.pluginKey);
74+ if (!invoker?.resolveAnnotations) return undefined;
75+ return yield* invoker.resolveAnnotations(toolId);
76+ }),
77+
78+ invoke: (toolId: ToolId, args: unknown, options: InvokeOptions) =>
7079 Effect.gen(function* () {
7180 const tool = yield* Effect.fromNullable(tools.get(toolId)).pipe(
7281 Effect.mapError(() => new ToolNotFoundError({ toolId })),
packages/core/sdk/src/index.test.ts+10−7
@@ -11,9 +11,12 @@ import { · 10 unmodified lines
1111 ElicitationResponse,
1212 type MemoryToolContext,
1313 type ToolId,
14+ type InvokeOptions,
1415 SecretId,
1516 } from "./index";
1617
18+const autoApprove: InvokeOptions = { onElicitation: "accept-all" };
19+
1720 // ---------------------------------------------------------------------------
1821 // Schemas
1922 // ---------------------------------------------------------------------------
@@ -95,7 +98,7 @@ describe("SDK Executor", () => { · 75 unmodified lines
9598 }),
9699 );
97100
98− const result = yield* executor.tools.invoke("inventory.getItem", { itemId: 42 });
101+ const result = yield* executor.tools.invoke("inventory.getItem", { itemId: 42 }, autoApprove);
99102 expect(result.data).toEqual({ id: 42, name: "Widget" });
100103 expect(result.error).toBeNull();
101104 }),
@@ -121,12 +124,12 @@ describe("SDK Executor", () => { · 19 unmodified lines
121124 );
122125
123126 const exit = yield* executor.tools
124− .invoke("inventory.getItem", { itemId: "not-a-number" })
127+ .invoke("inventory.getItem", { itemId: "not-a-number" }, autoApprove)
125128 .pipe(Effect.exit);
126129
127130 expect(Exit.isFailure(exit)).toBe(true);
128131 const error = yield* Effect.flip(
129− executor.tools.invoke("inventory.getItem", { itemId: "not-a-number" }),
132+ executor.tools.invoke("inventory.getItem", { itemId: "not-a-number" }, autoApprove),
130133 );
131134 expect(error._tag).toBe("ToolInvocationError");
132135 }),
@@ -136,7 +139,7 @@ describe("SDK Executor", () => { · 3 unmodified lines
136139 Effect.gen(function* () {
137140 const executor = yield* createExecutor(makeTestConfig());
138141 const error = yield* Effect.flip(
139− executor.tools.invoke("nonexistent", {}),
142+ executor.tools.invoke("nonexistent", {}, autoApprove),
140143 );
141144 expect(error._tag).toBe("ToolNotFoundError");
142145 }),
@@ -204,7 +207,7 @@ describe("SDK Executor", () => { · 61 unmodified lines
204207 expect(tools).toHaveLength(1);
205208 expect(tools[0]!.name).toBe("dynamicTool");
206209
207− const result = yield* executor.tools.invoke("runtime.dynamicTool", {});
210+ const result = yield* executor.tools.invoke("runtime.dynamicTool", {}, autoApprove);
208211 expect(result.data).toBe("dynamic result");
209212 }),
210213 );
@@ -352,7 +355,7 @@ describe("SDK Executor", () => { · 141 unmodified lines
352355 );
353356
354357 const error = yield* Effect.flip(
355− executor.tools.invoke("auth.login", {}),
358+ executor.tools.invoke("auth.login", {}, autoApprove),
356359 );
357360 expect(error._tag).toBe("ElicitationDeclinedError");
358361 }),
@@ -471,7 +474,7 @@ describe("SDK Executor", () => { · 112 unmodified lines
471474 const result = yield* executor.tools.invoke("vault.rotateKey", {
472475 secretName: "DB_PASSWORD",
473476 newValue: "correct-horse-battery-staple",
474− });
477+ }, autoApprove);
475478
476479 // 4. Verify the tool returned old and new values
477480 expect(result.data).toEqual({
packages/core/sdk/src/index.ts+1−0
@@ -17,6 +17,7 @@ export { · 16 unmodified lines
1717 ToolInvocationResult,
1818 ToolRegistry,
1919 ToolRegistration,
20+ ToolAnnotations,
2021 ToolListFilter,
2122 type ToolInvoker,
2223 type InvokeOptions,
packages/core/sdk/src/plugins/in-memory-tools.ts+7−2
@@ -12,6 +12,8 @@ import { · 11 unmodified lines
1212 } from "../tools";
1313 import {
1414 ElicitationDeclinedError,
15+ ElicitationResponse,
16+ type ElicitationHandler,
1517 type ElicitationRequest,
1618 } from "../elicitation";
1719 import { definePlugin, type PluginContext } from "../plugin";
@@ -192,13 +194,16 @@ const makeInvoker = ( · 174 unmodified lines
192194 },
193195 elicit: (request) =>
194196 Effect.gen(function* () {
195− const handler = options?.onElicitation;
196− if (!handler) {
197+ const raw = options?.onElicitation;
198+ if (!raw) {
197199 return yield* new ElicitationDeclinedError({
198200 toolId,
199201 action: "decline",
200202 });
201203 }
204+ const handler: ElicitationHandler = raw === "accept-all"
205+ ? () => Effect.succeed(new ElicitationResponse({ action: "accept" }))
206+ : raw;
202207 const response = yield* handler({
203208 toolId,
204209 args,
packages/core/sdk/src/tools.ts+23−4
@@ -11,6 +11,13 @@ import type { · 10 unmodified lines
1111 // Tool models
1212 // ---------------------------------------------------------------------------
1313
14+export class ToolAnnotations extends Schema.Class<ToolAnnotations>("ToolAnnotations")({
15+ /** Whether this tool requires user approval before execution */
16+ requiresApproval: Schema.optional(Schema.Boolean),
17+ /** Human-readable description shown in the approval prompt */
18+ approvalDescription: Schema.optional(Schema.String),
19+}) {}
20+
1421 export class ToolMetadata extends Schema.Class<ToolMetadata>("ToolMetadata")({
1522 id: ToolId,
1623 pluginKey: Schema.String,
@@ -51,8 +58,8 @@ export class ToolListFilter extends Schema.Class<ToolListFilter>("ToolListFilter · 34 unmodified lines
5158 // ---------------------------------------------------------------------------
5259
5360 export interface InvokeOptions {
54− /** Handler for elicitation requests. If not provided, elicitations are auto-declined. */
55− readonly onElicitation?: ElicitationHandler;
61+ /** Handler for elicitation requests, or "accept-all" to auto-approve everything. */
62+ readonly onElicitation: ElicitationHandler | "accept-all";
5663 }
5764
5865 // ---------------------------------------------------------------------------
@@ -71,7 +78,7 @@ export class ToolRegistry extends Context.Tag("@executor/sdk/ToolRegistry")< · 12 unmodified lines
7178 readonly invoke: (
7279 toolId: ToolId,
7380 args: unknown,
74− options?: InvokeOptions,
81+ options: InvokeOptions,
7582 ) => Effect.Effect<
7683 ToolInvocationResult,
7784 ToolNotFoundError | ToolInvocationError | ElicitationDeclinedError
@@ -100,6 +107,13 @@ export class ToolRegistry extends Context.Tag("@executor/sdk/ToolRegistry")< · 22 unmodified lines
100107 invoker: ToolInvoker,
101108 ) => Effect.Effect<void>;
102109
110+ /**
111+ * Resolve annotations for a tool by delegating to the plugin's invoker.
112+ */
113+ readonly resolveAnnotations: (
114+ toolId: ToolId,
115+ ) => Effect.Effect<ToolAnnotations | undefined>;
116+
103117 /** Register tools (used by plugins to push tools into the registry) */
104118 readonly register: (
105119 tools: readonly ToolRegistration[],
@@ -125,11 +139,16 @@ export interface ToolInvoker { · 19 unmodified lines
125139 readonly invoke: (
126140 toolId: ToolId,
127141 args: unknown,
128− options?: InvokeOptions,
142+ options: InvokeOptions,
129143 ) => Effect.Effect<
130144 ToolInvocationResult,
131145 ToolInvocationError | ElicitationDeclinedError
132146 >;
147+
148+ /** Dynamically compute annotations for a tool (e.g. approval requirements). */
149+ readonly resolveAnnotations?: (
150+ toolId: ToolId,
151+ ) => Effect.Effect<ToolAnnotations | undefined>;
133152 }
134153
135154 // ---------------------------------------------------------------------------
packages/core/storage-file/src/tool-registry.ts+10−1
@@ -99,7 +99,16 @@ export const makeKvToolRegistry = ( · 98 unmodified lines
9999 registerInvoker: (pluginKey: string, invoker: ToolInvoker) =>
100100 Effect.sync(() => { invokers.set(pluginKey, invoker); }),
101101
102− invoke: (toolId: ToolId, args: unknown, options?: InvokeOptions) =>
102+ resolveAnnotations: (toolId: ToolId) =>
103+ Effect.gen(function* () {
104+ const tool = yield* getTool(toolId);
105+ if (!tool) return undefined;
106+ const invoker = invokers.get(tool.pluginKey);
107+ if (!invoker?.resolveAnnotations) return undefined;
108+ return yield* invoker.resolveAnnotations(toolId);
109+ }),
110+
111+ invoke: (toolId: ToolId, args: unknown, options: InvokeOptions) =>
103112 Effect.gen(function* () {
104113 const tool = yield* getTool(toolId);
105114 if (!tool) return yield* new ToolNotFoundError({ toolId });
packages/plugins/openapi/src/sdk/invoke.ts+24−1
@@ -278,13 +278,36 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 277 unmodified lines
278278 // ToolInvoker — bridges operation store + HTTP client into SDK invoker
279279 // ---------------------------------------------------------------------------
280280
281+const SAFE_METHODS = new Set(["get", "head", "options"]);
282+
283+/**
284+ * Derive tool annotations from the HTTP method and path.
285+ */
286+export const annotationsForOperation = (method: string, pathTemplate: string): {
287+ requiresApproval?: boolean;
288+ approvalDescription?: string;
289+} => {
290+ if (SAFE_METHODS.has(method.toLowerCase())) return {};
291+ return {
292+ requiresApproval: true,
293+ approvalDescription: `${method.toUpperCase()} ${pathTemplate}`,
294+ };
295+};
296+
281297 export const makeOpenApiInvoker = (opts: {
282298 readonly operationStore: OpenApiOperationStore;
283299 readonly httpClientLayer: Layer.Layer<HttpClient.HttpClient>;
284300 readonly secrets: { readonly resolve: (secretId: SecretId, scopeId: ScopeId) => Effect.Effect<string, unknown> };
285301 readonly scopeId: ScopeId;
286302 }): ToolInvoker => ({
287− invoke: (toolId: ToolId, args: unknown, _options) =>
303+ resolveAnnotations: (toolId: ToolId) =>
304+ Effect.gen(function* () {
305+ const entry = yield* opts.operationStore.get(toolId);
306+ if (!entry) return undefined;
307+ return annotationsForOperation(entry.binding.method, entry.binding.pathTemplate);
308+ }),
309+
310+ invoke: (toolId: ToolId, args: unknown) =>
288311 Effect.gen(function* () {
289312 const entry = yield* opts.operationStore.get(toolId);
290313 if (!entry) {
packages/plugins/openapi/src/sdk/plugin.test.ts+7−4
@@ -11,9 +11,11 @@ import { · 10 unmodified lines
1111 } from "@effect/platform";
1212 import { NodeHttpServer } from "@effect/platform-node";
1313
14−import { createExecutor, makeTestConfig, SecretId } from "@executor/sdk";
14+import { createExecutor, makeTestConfig, SecretId, type InvokeOptions } from "@executor/sdk";
1515 import { openApiPlugin } from "./plugin";
1616
17+const autoApprove: InvokeOptions = { onElicitation: "accept-all" };
18+
1719 // ---------------------------------------------------------------------------
1820 // Define a test API with Effect HttpApi
1921 // ---------------------------------------------------------------------------
@@ -151,6 +153,7 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 131 unmodified lines
151153 const result = yield* executor.tools.invoke(
152154 "authed.items.echoHeaders",
153155 {},
156+ autoApprove,
154157 );
155158
156159 expect(result.error).toBeNull();
@@ -185,7 +188,7 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 28 unmodified lines
185188
186189 // Invoke — should fail with a clear error about the missing secret
187190 const error = yield* Effect.flip(
188− executor.tools.invoke("noauth.items.listItems", {}),
191+ executor.tools.invoke("noauth.items.listItems", {}, autoApprove),
189192 );
190193
191194 expect(error._tag).toBe("ToolInvocationError");
@@ -240,7 +243,7 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 48 unmodified lines
240243 baseUrl: "",
241244 });
242245
243− const result = yield* executor.tools.invoke("test.items.listItems", {});
246+ const result = yield* executor.tools.invoke("test.items.listItems", {}, autoApprove);
244247 expect(result.error).toBeNull();
245248 expect(result.data).toEqual(ITEMS);
246249 }),
@@ -267,7 +270,7 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 20 unmodified lines
267270
268271 const result = yield* executor.tools.invoke("test.items.getItem", {
269272 itemId: "2",
270− });
273+ }, autoApprove);
271274 expect(result.error).toBeNull();
272275 expect(result.data).toEqual({ id: 2, name: "Gadget" });
273276 }),
packages/plugins/openapi/src/sdk/plugin.ts+1−0
@@ -26,6 +26,7 @@ import { · 25 unmodified lines
2626 OperationBinding,
2727 } from "./types";
2828
29+
2930 // ---------------------------------------------------------------------------
3031 // Plugin config
3132 // ---------------------------------------------------------------------------