200 lines · 8.4 KB · TypeScript
RawDownload
// Cloud-only (billing): the pre-execution guards that sit in front of the MCP// session Durable Object's execution engine. Two independent gates run before// every new `execute`:////   - the BALANCE gate consults Autumn's `balances.check` and blocks once an//     org has used its plan's included executions, and//   - the RATE-LIMIT backstop counts executions per org per hour and blocks//     runaway automation even when billing is down.//// Both FAIL OPEN: an Autumn error or timeout must never take executions down// with the billing provider. These scenarios pin all four behaviours end to// end against the real workerd + McpSessionDO topology, driving a real MCP// client and reading Autumn's own ledger as ground truth — a blocked execution// must not reach the meter, and a failed-open one must show the check was// actually attempted (and faulted) rather than silently skipped.import { expect } from "@effect/vitest";import { Effect } from "effect"; import {  EXECUTION_LIMIT_BLOCKED_MESSAGE,  RATE_LIMIT_BLOCKED_MESSAGE,} from "../../apps/cloud/src/engine/execution-limit-messages";import { scenario } from "../src/scenario";import { Autumn, Billing, Mcp, Target } from "../src/services";import { E2E_EXECUTION_RATE_LIMIT } from "../setup/execution-limits";import type { Identity } from "../src/target"; const emailOf = (identity: Identity): string => identity.credentials?.email ?? identity.label; /** The org the bearer is scoped to — the Autumn customer id the guards decide *  against — read from the JWT's public claims. */const orgIdOf = (bearer: string): string => {  const claims = JSON.parse(Buffer.from(bearer.split(".")[1] ?? "", "base64url").toString()) as {    readonly org_id?: string;  };  if (!claims.org_id) throw new Error("orgIdOf: bearer carries no org_id claim");  return claims.org_id;}; // The e2e worker is booted with a small EXECUTION_RATE_LIMIT_PER_HOUR so the// backstop is reachable with real executions (cloud.boot.ts). The prod cap of// 1000/hour can't be. One shared constant (setup/execution-limits.ts) keeps// this and the boot env from drifting apart.const RATE_LIMIT = E2E_EXECUTION_RATE_LIMIT; scenario(  "Billing · an org out of executions is blocked before the code ever runs",  { timeout: 180_000 },  Effect.gen(function* () {    // Gates: billing is enforced here AND the Autumn ledger is observable.    // Yield before any work so a target missing either capability skips cleanly.    yield* Billing;    const autumn = yield* Autumn;    const target = yield* Target;    const mcp = yield* Mcp;     const identity = yield* target.newIdentity();    const bearer = yield* mcp.mintBearer(emailOf(identity));    const customerId = orgIdOf(bearer);     // Burn the whole included allotment so the org's remaining balance is zero.    // This records one `balances.track` event (the plan's included amount) —    // the baseline the blocked execution is measured against below.    yield* autumn.exhaustExecutions(customerId);    const afterExhaust = yield* autumn.usageEvents({ customerId, featureId: "executions" });    expect(afterExhaust.length, "exhausting the balance records exactly one usage event").toBe(1);     // A NEW MCP session => a fresh session DO => a fresh gate cache, so the    // balance is checked live (no 60s stale-allow window from a prior session).    const session = mcp.session(identity);    const result = yield* session.call("execute", { code: "return 1 + 1;" });     expect(result.ok, "a blocked execution surfaces as an MCP error result").toBe(false);    expect(result.text, "the client sees the execution-limit message").toContain(      EXECUTION_LIMIT_BLOCKED_MESSAGE,    );     // The meter is the source of truth: a blocked execution is neither run nor    // usage-tracked. Give any (erroneous) fire-and-forget track ample time to    // land, then assert nothing beyond the exhaust event was recorded.    const settle = yield* autumn.usageEvents({ customerId, featureId: "executions" });    expect(settle.length, "the blocked execution adds no 'executions' usage event").toBe(      afterExhaust.length,    );  }),); scenario(  "Billing · a balance-check failure fails open and still runs the execution",  { timeout: 180_000 },  Effect.gen(function* () {    yield* Billing;    const autumn = yield* Autumn;    const target = yield* Target;    const mcp = yield* Mcp;     const identity = yield* target.newIdentity();     yield* Effect.gen(function* () {      // One-shot 500 on the very next balances.check the gate makes.      yield* autumn.armFault({        match: { operationId: "balances.check" },        response: { status: 500, body: { message: "emulated billing outage" } },        times: 1,      });       const session = mcp.session(identity);      const result = yield* session.call("execute", { code: "return 6 * 7;" });       // Fail open: the outage must not block a solvent org's execution.      expect(result.ok, "the execution succeeds despite the billing outage").toBe(true);      expect(result.text, "it returns its value").toContain("42");       // Proof the gate actually CONSULTED Autumn and failed open (rather than      // never checking): the faulted balances.check is in the emulator ledger.      const checks = yield* autumn.ledgerFor("balances.check");      expect(        checks.some((entry) => entry.faulted),        "the gate's balances.check reached Autumn and was faulted",      ).toBe(true);    }).pipe(Effect.ensuring(autumn.clearFaults().pipe(Effect.ignore)));  }),); scenario(  "Billing · a balance-check timeout fails open and still runs the execution",  { timeout: 180_000 },  Effect.gen(function* () {    yield* Billing;    const autumn = yield* Autumn;    const target = yield* Target;    const mcp = yield* Mcp;     const identity = yield* target.newIdentity();     yield* Effect.gen(function* () {      // The armed check stalls 3s before returning — past the gate's 2s budget,      // so the gate times out client-side and fails open. (The response still      // arrives after the delay; the ~2s added latency fits the timeout.)      yield* autumn.armFault({        match: { operationId: "balances.check" },        response: { status: 200, body: { allowed: true } },        times: 1,        delayMs: 3000,      });       const session = mcp.session(identity);      const result = yield* session.call("execute", { code: "return 21 * 2;" });       expect(result.ok, "the execution succeeds despite the slow balance check").toBe(true);      expect(result.text, "it returns its value").toContain("42");       const checks = yield* autumn.ledgerFor("balances.check");      expect(checks.length, "the gate did attempt the (stalled) balances.check").toBeGreaterThan(0);    }).pipe(Effect.ensuring(autumn.clearFaults().pipe(Effect.ignore)));  }),); scenario(  "Billing · the rate-limit backstop blocks runaway executions and doesn't meter them",  { timeout: 180_000 },  Effect.gen(function* () {    yield* Billing;    const autumn = yield* Autumn;    const target = yield* Target;    const mcp = yield* Mcp;     const identity = yield* target.newIdentity();    const bearer = yield* mcp.mintBearer(emailOf(identity));    const customerId = orgIdOf(bearer);     // All executions run in ONE session so the balance cache stays allowed    // (the org is solvent) — only the per-org hourly counter should trip.    const session = mcp.session(identity);     // Run exactly the limit's worth of allowed executions.    for (let i = 1; i <= RATE_LIMIT; i++) {      const ok = yield* session.call("execute", { code: `return ${i};` });      expect(ok.ok, `execution ${i} (within the limit) succeeds`).toBe(true);    }     // The next one crosses the hourly cap: blocked with the backstop message.    const blocked = yield* session.call("execute", { code: "return 999;" });    expect(blocked.ok, "the execution past the cap is an MCP error result").toBe(false);    expect(blocked.text, "the client sees the rate-limit message").toContain(      RATE_LIMIT_BLOCKED_MESSAGE,    );     // A rate-limited execution is neither run nor metered: exactly the allowed    // runs reached the Autumn meter, and the blocked one added nothing.    const metered = yield* autumn.expectUsage({      customerId,      featureId: "executions",      count: RATE_LIMIT,    });    expect(metered.length, "only the allowed executions are metered — the blocked one is not").toBe(      RATE_LIMIT,    );  }),);