// ---------------------------------------------------------------------------// Metered execution stack: cloud's billing overlay over the execution seams.//// Cloud is the only host that meters executions, and BOTH of its execution// planes do so: the HTTP `/api/*` executor plane (api/protected.ts) and the MCP// session Durable Object (mcp/session-durable-object.ts). This module composes// the four billing-free `CloudExecutionSeamsLayer` seams with an `EngineDecorator`// that calls `AutumnService.trackExecution` after each execution.//// Keeping this in the cloud APP layer (not the neutral `engine/execution-stack.ts`)// is the billing-boundary line: the seams module names no billing service; the// metered overlay, provided ONLY here, does. Both planes import THIS layer and// supply `AutumnService` from their own context (boot for the HTTP plane,// `AutumnService.Default` locally for the DO).// ---------------------------------------------------------------------------import { Effect, Layer } from "effect";import { CodeExecutorProvider, DbProvider, EngineDecorator, HostConfig, PluginsProvider, type EngineStackIdentity,} from "@executor-js/api/server";import { AutumnService } from "../extensions/billing/service";import { hasPaidOrganizationSubscription } from "../extensions/billing/plans";import type { DbService } from "../db/db";import { CloudExecutionSeamsLayer } from "../engine/execution-stack";import { makeExecutionLimitGate } from "./execution-gate";import { makeCloudExecutionRateLimiter } from "./execution-rate-limit";import { withExecutionUsageTracking } from "./execution-usage";// Usage-metering decorator bound to the billing service, plus the two// pre-execution guards this layer owns, ordered cheapest first:// 1. rate-limit backstop (counter DO; free-tier abuse only — paid orgs are// exempt via the subscription lookup below)// 2. execution balance gate (Autumn check, cached 60s, fails open)// 3. usage tracking — fire-and-forget (`Effect.runFork`) so the billing// call can't stall a user-facing execution.// The guards wrap OUTSIDE the tracker so a blocked execution is neither run// nor tracked. One gate/limiter instance per layer build: the balance cache is// shared by every engine this decorator produces (in the MCP session DO that's// the session's engines; on the HTTP plane the boot layer builds it once).export const CloudMeteringEngineDecorator: Layer.Layer<EngineDecorator, never, AutumnService> = Layer.effect(EngineDecorator)( Effect.map(AutumnService.asEffect(), (autumn): EngineDecorator["Service"] => { const balanceGate = makeExecutionLimitGate((organizationId) => autumn.checkExecutionBalance(organizationId), ); // The limiter's paid-org exemption. This is the billing coupling the // limiter module deliberately avoids owning, and it reads the same // `PAID_AUTUMN_PLAN_IDS` config as the org-creation and seat gates so // "paid" means one thing across the app. The limiter calls this only // for orgs already over the cap and caches the answer, so the extra // Autumn round trip stays off the hot path. const rateLimiter = makeCloudExecutionRateLimiter((organizationId) => Effect.map( autumn.use((client) => client.customers.getOrCreate({ customerId: organizationId })), (customer) => hasPaidOrganizationSubscription(customer.subscriptions), ), ); return { decorate: (engine, identity: EngineStackIdentity) => rateLimiter.decorate( identity.organizationId, balanceGate.decorate( identity.organizationId, withExecutionUsageTracking(identity.organizationId, engine, (organizationId) => Effect.runFork(autumn.trackExecution(organizationId)), ), ), ), }; }), );/** * The metered execution stack used by BOTH cloud planes (HTTP executor plane and * MCP session DO): the four billing-free `CloudExecutionSeamsLayer` seams plus * the billing decorator. Requires `DbService` (per-request Hyperdrive db) and * `AutumnService` (usage metering) from the surrounding context. */export const CloudMeteredExecutionStackLayer: Layer.Layer< DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator, never, AutumnService | DbService> = Layer.merge(CloudExecutionSeamsLayer, CloudMeteringEngineDecorator);
// ---------------------------------------------------------------------------
// Metered execution stack: cloud's billing overlay over the execution seams.
//
// Cloud is the only host that meters executions, and BOTH of its execution
// planes do so: the HTTP `/api/*` executor plane (api/protected.ts) and the MCP
// session Durable Object (mcp/session-durable-object.ts). This module composes
// the four billing-free `CloudExecutionSeamsLayer` seams with an `EngineDecorator`
// that calls `AutumnService.trackExecution` after each execution.
//
// Keeping this in the cloud APP layer (not the neutral `engine/execution-stack.ts`)
// is the billing-boundary line: the seams module names no billing service; the
// metered overlay, provided ONLY here, does. Both planes import THIS layer and
// supply `AutumnService` from their own context (boot for the HTTP plane,
// `AutumnService.Default` locally for the DO).
// ---------------------------------------------------------------------------
import { Effect, Layer } from "effect";
import {
CodeExecutorProvider,
DbProvider,
EngineDecorator,
HostConfig,
PluginsProvider,
type EngineStackIdentity,
} from "@executor-js/api/server";
import { AutumnService } from "../extensions/billing/service";
import { hasPaidOrganizationSubscription } from "../extensions/billing/plans";
import type { DbService } from "../db/db";
import { CloudExecutionSeamsLayer } from "../engine/execution-stack";
import { makeExecutionLimitGate } from "./execution-gate";
import { makeCloudExecutionRateLimiter } from "./execution-rate-limit";
import { withExecutionUsageTracking } from "./execution-usage";
// Usage-metering decorator bound to the billing service, plus the two
// pre-execution guards this layer owns, ordered cheapest first:
// 1. rate-limit backstop (counter DO; free-tier abuse only — paid orgs are
// exempt via the subscription lookup below)
// 2. execution balance gate (Autumn check, cached 60s, fails open)
// 3. usage tracking — fire-and-forget (`Effect.runFork`) so the billing
// call can't stall a user-facing execution.
// The guards wrap OUTSIDE the tracker so a blocked execution is neither run
// nor tracked. One gate/limiter instance per layer build: the balance cache is
// shared by every engine this decorator produces (in the MCP session DO that's
// the session's engines; on the HTTP plane the boot layer builds it once).
export const CloudMeteringEngineDecorator: Layer.Layer<EngineDecorator, never, AutumnService> =
Layer.effect(EngineDecorator)(
Effect.map(AutumnService.asEffect(), (autumn): EngineDecorator["Service"] => {
const balanceGate = makeExecutionLimitGate((organizationId) =>
autumn.checkExecutionBalance(organizationId),
);
// The limiter's paid-org exemption. This is the billing coupling the
// limiter module deliberately avoids owning, and it reads the same
// `PAID_AUTUMN_PLAN_IDS` config as the org-creation and seat gates so
// "paid" means one thing across the app. The limiter calls this only
// for orgs already over the cap and caches the answer, so the extra
// Autumn round trip stays off the hot path.
const rateLimiter = makeCloudExecutionRateLimiter((organizationId) =>
Effect.map(
autumn.use((client) => client.customers.getOrCreate({ customerId: organizationId })),
(customer) => hasPaidOrganizationSubscription(customer.subscriptions),
),
);
return {
decorate: (engine, identity: EngineStackIdentity) =>
rateLimiter.decorate(
identity.organizationId,
balanceGate.decorate(
identity.organizationId,
withExecutionUsageTracking(identity.organizationId, engine, (organizationId) =>
Effect.runFork(autumn.trackExecution(organizationId)),
),
),
),
};
}),
);
/**
* The metered execution stack used by BOTH cloud planes (HTTP executor plane and
* MCP session DO): the four billing-free `CloudExecutionSeamsLayer` seams plus
* the billing decorator. Requires `DbService` (per-request Hyperdrive db) and
* `AutumnService` (usage metering) from the surrounding context.
*/
export const CloudMeteredExecutionStackLayer: Layer.Layer<
DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator,
never,
AutumnService | DbService
> = Layer.merge(CloudExecutionSeamsLayer, CloudMeteringEngineDecorator);