- cloud: resolves userId from the authenticated session and builds a [userOrgScope, orgScope] stack via createScopedExecutor(userId, orgId, orgName). userId threaded through execution-stack, mcp-session, and MCP dispatch init. - cloud env refactor: deletes the lazy Proxy apps/cloud/src/env.ts in favor of cloudflare:workers env imported at call sites; adds env-augment.d.ts to type secrets not present in generated wrangler types. - cloud/server.ts: env-guarded OTEL wiring (skips instrument() when AXIOM_TOKEN absent); captures rawFetch before instrument() mutates it so dev requests still land on the Hono app. - cloud/services/db.ts: prefers DATABASE_URL over the hyperdrive proxy so local dev works without the tunnel. - vite.config.ts: devCrashGuard plugin that prints uncaught Worker errors with a stable exit. - hosts/mcp: optional debug flag surfaces verbose session logging, wired up from cloud mcp-session. - local/examples/marketing: migrated to scopes: [scope].
Showing the changes to apps/cloud/src/services (6 of 25 files). Show all
Unified · Split
| @@ -181,7 +181,7 @@ const createTestScopedExecutor = (scopeId: string, scopeName: string) => · 180 unmodified lines | ||
| 181 | 181 | name: scopeName, |
| 182 | 182 | createdAt: new Date(), |
| 183 | 183 | }); |
| 184 | − return yield* createExecutor({ scope, adapter, blobs, plugins }); | |
| 184 | + return yield* createExecutor({ scopes: [scope], adapter, blobs, plugins }); | |
| 185 | 185 | }); |
| 186 | 186 | |
| 187 | 187 | // --------------------------------------------------------------------------- |
| @@ -2,12 +2,11 @@ · 1 unmodified line | ||
| 2 | 2 | // Autumn billing service — wraps the autumn-js SDK with Effect |
| 3 | 3 | // --------------------------------------------------------------------------- |
| 4 | 4 | |
| 5 | +import { env } from "cloudflare:workers"; | |
| 5 | 6 | import * as Sentry from "@sentry/cloudflare"; |
| 6 | 7 | import { Autumn } from "autumn-js"; |
| 7 | 8 | import { Context, Data, Effect, Layer } from "effect"; |
| 8 | 9 | |
| 9 | −import { server } from "../env"; | |
| 10 | − | |
| 11 | 10 | // --------------------------------------------------------------------------- |
| 12 | 11 | // Errors |
| 13 | 12 | // --------------------------------------------------------------------------- |
| @@ -36,7 +35,7 @@ export type IAutumnService = Readonly<{ · 22 unmodified lines | ||
| 36 | 35 | // --------------------------------------------------------------------------- |
| 37 | 36 | |
| 38 | 37 | const make = Effect.sync(() => { |
| 39 | − const secretKey = server.AUTUMN_SECRET_KEY; | |
| 38 | + const secretKey = env.AUTUMN_SECRET_KEY; | |
| 40 | 39 | |
| 41 | 40 | if (!secretKey) { |
| 42 | 41 | const notConfigured = Effect.die( |
| @@ -18,7 +18,6 @@ import type { PgDatabase } from "drizzle-orm/pg-core"; · 17 unmodified lines | ||
| 18 | 18 | import postgres, { type Sql } from "postgres"; |
| 19 | 19 | import * as cloudSchema from "./schema"; |
| 20 | 20 | import * as executorSchema from "./executor-schema"; |
| 21 | −import { server } from "../env"; | |
| 22 | 21 | |
| 23 | 22 | // Exported so every drizzle() call in the cloud app shares one schema |
| 24 | 23 | // object. Historically `mcp-session.ts` built its own and forgot to spread |
| @@ -38,10 +37,7 @@ const resolveConnectionString = () => { · 13 unmodified lines | ||
| 38 | 37 | // In local dev prefer an explicit DATABASE_URL (direct connection to |
| 39 | 38 | // the PGlite socket server) so we bypass Miniflare's Hyperdrive proxy. |
| 40 | 39 | // In production fall back to the Hyperdrive binding. |
| 41 | − if (server.DATABASE_URL) { | |
| 42 | − return server.DATABASE_URL; | |
| 43 | − } | |
| 44 | − return env.HYPERDRIVE?.connectionString ?? server.DATABASE_URL; | |
| 40 | + return env.DATABASE_URL || env.HYPERDRIVE?.connectionString || ""; | |
| 45 | 41 | }; |
| 46 | 42 | |
| 47 | 43 | const makeSql = (): Sql => |
| @@ -14,11 +14,17 @@ import { withExecutionUsageTracking } from "../api/execution-usage"; · 13 unmodified lines | ||
| 14 | 14 | import { AutumnService } from "./autumn"; |
| 15 | 15 | import { createScopedExecutor } from "./executor"; |
| 16 | 16 | |
| 17 | −export const makeExecutionStack = (organizationId: string, organizationName: string) => | |
| 17 | +export const makeExecutionStack = ( | |
| 18 | + userId: string, | |
| 19 | + organizationId: string, | |
| 20 | + organizationName: string, | |
| 21 | +) => | |
| 18 | 22 | Effect.gen(function* () { |
| 19 | − const executor = yield* createScopedExecutor(organizationId, organizationName).pipe( | |
| 20 | − Effect.withSpan("McpSessionDO.createScopedExecutor"), | |
| 21 | − ); | |
| 23 | + const executor = yield* createScopedExecutor( | |
| 24 | + userId, | |
| 25 | + organizationId, | |
| 26 | + organizationName, | |
| 27 | + ).pipe(Effect.withSpan("McpSessionDO.createScopedExecutor")); | |
| 22 | 28 | const codeExecutor = makeDynamicWorkerExecutor({ loader: env.LOADER }); |
| 23 | 29 | const autumn = yield* AutumnService; |
| 24 | 30 | const engine = withExecutionUsageTracking( |
| @@ -25,8 +25,8 @@ import { mcpPlugin } from "@executor/plugin-mcp"; · 24 unmodified lines | ||
| 25 | 25 | import { graphqlPlugin } from "@executor/plugin-graphql"; |
| 26 | 26 | import { workosVaultPlugin } from "@executor/plugin-workos-vault"; |
| 27 | 27 | |
| 28 | +import { env } from "cloudflare:workers"; | |
| 28 | 29 | import { DbService } from "./db"; |
| 29 | −import { server } from "../env"; | |
| 30 | 30 | |
| 31 | 31 | // --------------------------------------------------------------------------- |
| 32 | 32 | // Plugin list — one place, used for both the runtime and the CLI config |
| @@ -45,24 +45,31 @@ const createOrgPlugins = () => · 12 unmodified lines | ||
| 45 | 45 | graphqlPlugin(), |
| 46 | 46 | workosVaultPlugin({ |
| 47 | 47 | credentials: { |
| 48 | − apiKey: server.WORKOS_API_KEY, | |
| 49 | − clientId: server.WORKOS_CLIENT_ID, | |
| 48 | + apiKey: env.WORKOS_API_KEY, | |
| 49 | + clientId: env.WORKOS_CLIENT_ID, | |
| 50 | 50 | }, |
| 51 | 51 | }), |
| 52 | 52 | ] as const; |
| 53 | 53 | |
| 54 | 54 | // --------------------------------------------------------------------------- |
| 55 | −// Create a fresh executor for a scope (stateless, per-request). | |
| 55 | +// Create a fresh executor for a (user, org) pair (stateless, per-request). | |
| 56 | 56 | // |
| 57 | −// Today "scope" is the WorkOS organization — orgs are the only scope | |
| 58 | −// level the cloud app exposes. When workspace / user scopes land, this | |
| 59 | −// function grows to accept a `ScopeStack` instead of a single scope id, | |
| 60 | −// and nothing downstream (executor, plugins, storage) has to change. | |
| 57 | +// Scope stack is `[userOrgScope, orgScope]` — innermost first. The | |
| 58 | +// user-within-org scope id (`user-org:${userId}:${orgId}`) intentionally | |
| 59 | +// includes the org id so the same WorkOS user in a different org gets a | |
| 60 | +// distinct scope row; future workspace scopes can slot in between without | |
| 61 | +// conflicting with a hypothetical global user scope. | |
| 62 | +// | |
| 63 | +// OAuth tokens land at `ctx.scopes[0]` (the user-org scope) by default, so | |
| 64 | +// a member's access/refresh tokens can't leak to other members via | |
| 65 | +// `secrets.list`, while source rows and org-wide credentials live on the | |
| 66 | +// outer scope. | |
| 61 | 67 | // --------------------------------------------------------------------------- |
| 62 | 68 | |
| 63 | 69 | export const createScopedExecutor = ( |
| 64 | − scopeId: string, | |
| 65 | − scopeName: string, | |
| 70 | + userId: string, | |
| 71 | + organizationId: string, | |
| 72 | + organizationName: string, | |
| 66 | 73 | ) => |
| 67 | 74 | Effect.gen(function* () { |
| 68 | 75 | const { db } = yield* DbService; |
| @@ -72,9 +79,14 @@ export const createScopedExecutor = ( · 3 unmodified lines | ||
| 72 | 79 | const adapter = makePostgresAdapter({ db, schema }); |
| 73 | 80 | const blobs = makePostgresBlobStore({ db }); |
| 74 | 81 | |
| 75 | − const scope = new Scope({ | |
| 76 | − id: ScopeId.make(scopeId), | |
| 77 | − name: scopeName, | |
| 82 | + const orgScope = new Scope({ | |
| 83 | + id: ScopeId.make(organizationId), | |
| 84 | + name: organizationName, | |
| 85 | + createdAt: new Date(), | |
| 86 | + }); | |
| 87 | + const userOrgScope = new Scope({ | |
| 88 | + id: ScopeId.make(`user-org:${userId}:${organizationId}`), | |
| 89 | + name: `Personal · ${organizationName}`, | |
| 78 | 90 | createdAt: new Date(), |
| 79 | 91 | }); |
| 80 | 92 | |
| @@ -82,5 +94,10 @@ export const createScopedExecutor = ( · 1 unmodified line | ||
| 82 | 94 | // the opaque `InternalError({ traceId })` happens at the HTTP edge |
| 83 | 95 | // via `withCapture` (see `api/protected-layers.ts`). That's |
| 84 | 96 | // where `ErrorCaptureLive` (Sentry) gets wired in. |
| 85 | − return yield* createExecutor({ scope, adapter, blobs, plugins }); | |
| 97 | + return yield* createExecutor({ | |
| 98 | + scopes: [userOrgScope, orgScope], | |
| 99 | + adapter, | |
| 100 | + blobs, | |
| 101 | + plugins, | |
| 102 | + }); | |
| 86 | 103 | }); |
| @@ -53,10 +53,9 @@ import { · 52 unmodified lines | ||
| 53 | 53 | ATTR_SERVICE_NAME, |
| 54 | 54 | ATTR_SERVICE_VERSION, |
| 55 | 55 | } from "@opentelemetry/semantic-conventions"; |
| 56 | +import { env } from "cloudflare:workers"; | |
| 56 | 57 | import { Effect, Layer } from "effect"; |
| 57 | 58 | |
| 58 | −import { server } from "../env"; | |
| 59 | − | |
| 60 | 59 | const SERVICE_NAME = "executor-cloud"; |
| 61 | 60 | const SERVICE_VERSION = "1.0.0"; |
| 62 | 61 | |
| @@ -71,7 +70,7 @@ export const TelemetryLive: Layer.Layer<never> = OtelTracer.layerGlobal.pipe( · 8 unmodified lines | ||
| 71 | 70 | let installed = false; |
| 72 | 71 | const ensureGlobalTracerProvider = (): boolean => { |
| 73 | 72 | if (installed) return true; |
| 74 | − if (!server.AXIOM_TOKEN) return false; | |
| 73 | + if (!env.AXIOM_TOKEN) return false; | |
| 75 | 74 | const provider = new WebTracerProvider({ |
| 76 | 75 | resource: resourceFromAttributes({ |
| 77 | 76 | [ATTR_SERVICE_NAME]: SERVICE_NAME, |
| @@ -80,10 +79,10 @@ const ensureGlobalTracerProvider = (): boolean => { · 2 unmodified lines | ||
| 80 | 79 | spanProcessors: [ |
| 81 | 80 | new SimpleSpanProcessor( |
| 82 | 81 | new OTLPTraceExporter({ |
| 83 | − url: server.AXIOM_TRACES_URL, | |
| 82 | + url: env.AXIOM_TRACES_URL ?? "https://api.axiom.co/v1/traces", | |
| 84 | 83 | headers: { |
| 85 | − Authorization: `Bearer ${server.AXIOM_TOKEN}`, | |
| 86 | − "X-Axiom-Dataset": server.AXIOM_DATASET, | |
| 84 | + Authorization: `Bearer ${env.AXIOM_TOKEN}`, | |
| 85 | + "X-Axiom-Dataset": env.AXIOM_DATASET ?? "executor-cloud", | |
| 87 | 86 | }, |
| 88 | 87 | }), |
| 89 | 88 | ), |