History

Wire execution guards into the metered engine decorator

Order: rate-limit backstop first (cheap counter), then the balance gate, then usage tracking. Guards wrap outside the tracker so a blocked execution is neither run nor tracked. Covers both planes (HTTP executor plane and MCP session DO) since both build engines through this layer.

Rhys Sullivan committed 4 months ago

Commit bc95e261 parent 1d8d9d01 file · +30 −9Browse files

Unified · Split

apps/cloud/src/engine/execution-stack-metered.ts+30−9
@@ -28,19 +28,40 @@ import { · 27 unmodified lines
2828 import { AutumnService } from "../extensions/billing/service";
2929 import type { DbService } from "../db/db";
3030 import { CloudExecutionSeamsLayer } from "../engine/execution-stack";
31+import { makeExecutionLimitGate } from "./execution-gate";
32+import { makeCloudExecutionRateLimiter } from "./execution-rate-limit";
3133 import { withExecutionUsageTracking } from "./execution-usage";
3234
33−// Usage-metering decorator bound to the billing service. `trackExecution` is
34−// fire-and-forget (`Effect.runFork`) so the billing call can't stall a
35−// user-facing execution.
35+// Usage-metering decorator bound to the billing service, plus the two
36+// pre-execution guards this layer owns, ordered cheapest first:
37+// 1. rate-limit backstop (counter DO, independent of billing)
38+// 2. execution balance gate (Autumn check, cached 60s, fails open)
39+// 3. usage tracking — fire-and-forget (`Effect.runFork`) so the billing
40+// call can't stall a user-facing execution.
41+// The guards wrap OUTSIDE the tracker so a blocked execution is neither run
42+// nor tracked. One gate/limiter instance per layer build: the balance cache is
43+// shared by every engine this decorator produces (in the MCP session DO that's
44+// the session's engines; on the HTTP plane the boot layer builds it once).
3645 export const CloudMeteringEngineDecorator: Layer.Layer<EngineDecorator, never, AutumnService> =
3746 Layer.effect(EngineDecorator)(
38− Effect.map(AutumnService.asEffect(), (autumn): EngineDecorator["Service"] => ({
39− decorate: (engine, identity: EngineStackIdentity) =>
40− withExecutionUsageTracking(identity.organizationId, engine, (organizationId) =>
41− Effect.runFork(autumn.trackExecution(organizationId)),
42− ),
43− })),
47+ Effect.map(AutumnService.asEffect(), (autumn): EngineDecorator["Service"] => {
48+ const balanceGate = makeExecutionLimitGate((organizationId) =>
49+ autumn.checkExecutionBalance(organizationId),
50+ );
51+ const rateLimiter = makeCloudExecutionRateLimiter();
52+ return {
53+ decorate: (engine, identity: EngineStackIdentity) =>
54+ rateLimiter.decorate(
55+ identity.organizationId,
56+ balanceGate.decorate(
57+ identity.organizationId,
58+ withExecutionUsageTracking(identity.organizationId, engine, (organizationId) =>
59+ Effect.runFork(autumn.trackExecution(organizationId)),
60+ ),
61+ ),
62+ ),
63+ };
64+ }),
4465 );
4566
4667 /**