109 lines · 4.1 KB · TypeScript
RawDownload
// Cloud-only: auth failure propagation through the full execution stack —// HTTP API → execution engine → sandbox code → OpenAPI tool invocation. When a// connection's credential value cannot resolve (a `from` reference to a vault// item that was never stored), invoking one of its tools must surface a// structured, model-visible `connection_value_missing` auth failure — not an// opaque internal tool error — so an agent can tell the user to re-connect.import { randomBytes } from "node:crypto"; import { expect } from "@effect/vitest";import { Effect } from "effect";import { composePluginApi } from "@executor-js/api/server";import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";import {  AuthTemplateSlug,  ConnectionName,  IntegrationSlug,  ProviderItemId,} from "@executor-js/sdk/shared"; import { scenario } from "../src/scenario"; const api = composePluginApi([openApiHttpPlugin()] as const); /** Minimal OpenAPI spec with a single GET /ping. The base URL is never *  reached: credential resolution fails before any HTTP call is made. */const pingSpec = JSON.stringify({  openapi: "3.0.3",  info: { title: "Ping API", version: "1.0.0" },  paths: {    "/ping": {      get: { operationId: "ping", summary: "Ping", responses: { "200": { description: "pong" } } },    },  },}); scenario(  "Executions · a tool call with an unresolvable credential fails with connection_value_missing",  { needs: ["api"] },  (ctx) =>    Effect.gen(function* () {      const identity = yield* ctx.target.newIdentity();      const client = yield* ctx.api.client(api, identity);       // Identifier-safe slug: it becomes a property path in the sandbox code.      const slug = IntegrationSlug.make(`authscn${randomBytes(4).toString("hex")}`);      yield* client.openapi.addSpec({        payload: {          spec: { kind: "blob", value: pingSpec },          slug,          baseUrl: "https://api.example.test",          authenticationTemplate: [            {              slug: "apiKey",              type: "apiKey",              headers: { Authorization: ["Bearer ", { type: "variable", name: "token" }] },            },          ],        },      });       // A connection whose value cannot resolve: a `from` reference into the      // real credential provider, pointing at an item that was never stored.      const providers = yield* client.providers.list();      expect(providers.length, "a credential provider is registered").toBeGreaterThan(0);      yield* client.connections.create({        payload: {          owner: "org",          name: ConnectionName.make("main"),          integration: slug,          template: AuthTemplateSlug.make("apiKey"),          from: { provider: providers[0]!, id: ProviderItemId.make(`${slug}-never-stored`) },        },      });       // The tool is in the catalog; its address is the sandbox call path.      const tools = yield* client.tools.list();      const tool = tools.find((entry) => String(entry.integration) === String(slug));      expect(tool?.address, "the integration's tool is in the catalog").toBeDefined();       const execution = yield* client.executions.execute({        payload: {          code: [`const result = await ${tool!.address}({});`, "return result;"].join("\n"),        },      });       expect(execution.status, "the execution itself completes").toBe("completed");      if (execution.status !== "completed") return; // unreachable — narrowing only      expect(execution.isError, "an auth failure is a model-visible result, not a tool error").toBe(        false,      );      expect(        JSON.stringify(execution.structured).toLowerCase(),        "no opaque internal tool error reaches the model",      ).not.toContain("internal tool error");      expect(        execution.structured,        "the model sees a structured authentication failure it can act on",      ).toMatchObject({        status: "completed",        result: {          ok: false,          error: {            code: "connection_value_missing",            details: { category: "authentication" },          },        },      });    }),);