diff --git a/benchmarks/dynamic-apps/src/edge.ts b/benchmarks/dynamic-apps/src/edge.ts index 0db83f9d6..36506772f 100644 --- a/benchmarks/dynamic-apps/src/edge.ts +++ b/benchmarks/dynamic-apps/src/edge.ts @@ -105,7 +105,6 @@ export function createBenchmarkApplication(): Hono { headers.set("x-agentos-app-registry-dispatch", "1"); return appsRouter.fetch(new Request(request, { headers })); }; - app.all("/api/rivet", (c) => privateRegistry(c.req.raw)); app.all("/api/rivet/*", (c) => privateRegistry(c.req.raw)); app.all("/bench/noop", () => { diff --git a/benchmarks/dynamic-apps/src/fixture.ts b/benchmarks/dynamic-apps/src/fixture.ts index cf209ac8b..5a0c42b7e 100644 --- a/benchmarks/dynamic-apps/src/fixture.ts +++ b/benchmarks/dynamic-apps/src/fixture.ts @@ -68,10 +68,12 @@ export async function deployActorBenchmarkFixture( type: "module", main: "index.js", dependencies: { + hono: "4.13.3", rivetkit: "2.3.11", }, }), "index.js": ` +import { Hono } from "hono"; import { actor, event, setup } from "rivetkit"; import { db } from "rivetkit/db"; @@ -106,12 +108,11 @@ const counter = actor({ }, }); -export const registry = setup({ use: { counter } }); -registry.start(); - -export default function fetch() { - return Response.json({ ok: true, workload: "actor-and-direct-http" }); -} +const registry = setup({ use: { counter } }); +const app = new Hono(); +app.all("/api/rivet/*", (c) => registry.handler(c.req.raw)); +app.all("*", () => Response.json({ ok: true, workload: "actor-and-direct-http" })); +export default app; `, }, scaling: { diff --git a/benchmarks/dynamic-apps/src/runtime-stress.ts b/benchmarks/dynamic-apps/src/runtime-stress.ts index 500722abf..720257581 100644 --- a/benchmarks/dynamic-apps/src/runtime-stress.ts +++ b/benchmarks/dynamic-apps/src/runtime-stress.ts @@ -211,11 +211,9 @@ async function main(): Promise { ); const actorHandlerStallArtifact = await createActorArtifact( "actor-handler-stall", - `export const registry = { - handler() { + `export function handler() { while (true) {} - }, -};`, +}`, ); const actorMemoryArtifact = await createActorArtifact( "actor-memory", @@ -1274,26 +1272,22 @@ function createActorArtifact( function actorTrafficSource(): string { return ` let counter = 0; -export const registry = { - async handler(request) { +export async function handler(request) { counter += 1; const requestBytes = (await request.arrayBuffer()).byteLength; return Response.json({ counter, requestBytes }); - }, -}; +} `; } function actorMemorySource(allocationBytes: number): string { return ` let retained; -export const registry = { - handler() { +export function handler() { retained = new Uint8Array(${allocationBytes}); retained.fill(1); return new Response(String(retained.byteLength)); - }, -}; +} `; } diff --git a/packages/dynamic-apps-builder/test/builder.test.ts b/packages/dynamic-apps-builder/test/builder.test.ts index 419b3be05..61789364e 100644 --- a/packages/dynamic-apps-builder/test/builder.test.ts +++ b/packages/dynamic-apps-builder/test/builder.test.ts @@ -84,7 +84,6 @@ describe("apps-builder", () => { entrypoint: "app.ts", release: "rivetkit-direct-test", maxResponseBytes: 1024 * 1024, - usesRivetKit: true, }), ); await writeFile( @@ -92,10 +91,10 @@ describe("apps-builder", () => { [ 'import { actor, setup } from "rivetkit";', "const counter = actor({ state: { count: 0 } });", - "export const registry = setup({ use: { counter } });", - "registry.start();", + "const registry = setup({ use: { counter } });", "export default {", - " fetch() {", + " fetch(request) {", + ' if (new URL(request.url).pathname.startsWith("/api/rivet")) return registry.handler(request);', ' return new Response(typeof registry.handler + ":" + typeof counter);', " },", "};", @@ -283,7 +282,7 @@ describe("apps-builder", () => { }); }); - test("emits a small platform-linked actor registry bundle", async () => { + test("emits a small platform-linked actor fetch bundle", async () => { const root = await mkdtemp(join(tmpdir(), "agentos-apps-actor-builder-")); const workspace = join(root, "workspace"); const release = join(root, "release"); @@ -297,8 +296,8 @@ describe("apps-builder", () => { [ 'import { actor, setup } from "rivetkit";', "const counter = actor({ state: { count: 0 } });", - "export const registry = setup({ use: { counter } });", - "registry.start();", + "const registry = setup({ use: { counter } });", + "export default { fetch: (request) => registry.handler(request) };", ].join("\n"), ); const configPath = join(root, "config.json"); diff --git a/packages/dynamic-apps/API_CONTRACT.md b/packages/dynamic-apps/API_CONTRACT.md index de5cd6df1..9ccfd805d 100644 --- a/packages/dynamic-apps/API_CONTRACT.md +++ b/packages/dynamic-apps/API_CONTRACT.md @@ -342,10 +342,10 @@ part of the retained API. ## App-defined RivetKit actor contract -An application that declares `rivetkit` may export -`const registry = setup(...)` and may retain its `registry.start()` call. The -platform suppresses that call while importing the managed actor bundle. The -same app must still provide a valid direct default fetch handler. +An application that declares `rivetkit` mounts `registry.handler()` at +`/api/rivet` and `/api/rivet/*` in its default exported fetch router. It must +not call `registry.start()` or `serve()` because Dynamic Apps owns the HTTP +listener. The same mounted router serves ordinary direct requests. On activation, deployment configures the returned `namespace` and `pool` with an authenticated serverless callback to the private `agentOSAppsApp` actor. @@ -355,7 +355,7 @@ actor-enabled release. The callback lazily verifies and extracts `actor/main.mjs`, then caches one worker thread per active release. The worker uses the platform's pinned -RivetKit WebAssembly runtime and the app namespace/pool connection. Actor +RivetKit native runtime and the app namespace/pool connection. Actor state, actions, events, connections, request/response streaming, backpressure, and cancellation use the ordinary RivetKit protocol. Worker entries are bounded by count, V8 heap, idle TTL, callback body size, and container memory diff --git a/packages/dynamic-apps/README.md b/packages/dynamic-apps/README.md index 32ff54c72..186d538ca 100644 --- a/packages/dynamic-apps/README.md +++ b/packages/dynamic-apps/README.md @@ -34,11 +34,13 @@ bodies are not supported. ## App-defined actors -An app that declares `rivetkit` may also export a registry. The platform owns -the runner lifecycle, so the existing `registry.start()` call remains valid: +An app that declares `rivetkit` mounts the registry handler in its normal fetch +router. Dynamic Apps owns the listener, so the application must not call +`registry.start()` or `serve()`: ```ts import { actor, setup } from "rivetkit"; +import { Hono } from "hono"; const counter = actor({ state: { value: 0 }, @@ -49,10 +51,11 @@ const counter = actor({ }, }); -export const registry = setup({ use: { counter } }); -registry.start(); - -export default () => new Response("ok"); +const registry = setup({ use: { counter } }); +const app = new Hono(); +app.all("/api/rivet/*", (c) => registry.handler(c.req.raw)); +app.all("*", () => new Response("ok")); +export default app; ``` Use the unchanged `deployApp` result to create the app client: @@ -74,13 +77,27 @@ Every app is deployed to its own stable Rivet namespace. On Rivet Cloud, set uses it to provision the namespace and namespace-scoped access, secret, and publishable credentials. The management credential is never returned or passed to app code; `deployApp()` returns only the app's publishable token. -Rivet Compute derives the app actor callback from its `.rivet.run` hostname. -Set `DYNAMIC_APPS_CALLBACK_URL` only when the host is exposed at a different -public origin; Dynamic Apps appends `/api/rivet`. + +The deploy CLI's cached management credential is not injected into the app. +Pass the token as a server-side Compute environment variable: + +```sh +npx @rivetkit/cli deploy \ + --namespace \ + --env PORT=3000 \ + --env RIVET_CLOUD_TOKEN="$RIVET_CLOUD_TOKEN" +``` + +By default, the app actor callback enters through the host app actor's Rivet +gateway and authenticates with the publishable credential from +`RIVET_PUBLIC_ENDPOINT`. This keeps the child app namespace separate from the +host registry namespace. `DYNAMIC_APPS_CALLBACK_URL` is only for a custom +receiver that accepts child-namespace lifecycle callbacks; Dynamic Apps appends +`/api/rivet` to that origin. Actor requests follow the normal Rivet Engine path. The app's serverless callback loads its verified actor bundle into a bounded process-local worker -thread and uses the host's pinned RivetKit WebAssembly runtime. State, actions, +thread and uses the host's pinned RivetKit native runtime. State, actions, events, connections, and streaming actor responses are handled by RivetKit; ordinary HTTP for the same app still uses the agentOS evaluation path. diff --git a/packages/dynamic-apps/src/actor-runtime.ts b/packages/dynamic-apps/src/actor-runtime.ts index 901b9e83f..972c18825 100644 --- a/packages/dynamic-apps/src/actor-runtime.ts +++ b/packages/dynamic-apps/src/actor-runtime.ts @@ -198,6 +198,15 @@ export class DynamicActorRuntime { status: 413, }); } + console.info( + JSON.stringify({ + msg: "Dynamic App actor callback received", + method: input.request.method, + path: new URL(input.request.url).pathname, + bodyBytes: body.byteLength, + contentLength: input.request.headers.get("content-length"), + }), + ); const entry = await this.#entry(input); try { await entry.ready; @@ -396,10 +405,10 @@ export class DynamicActorRuntime { await mkdir(dirname(target), { recursive: true }); await writeFile(target, bytes, { mode: 0o600 }); } - const platformPackages = await resolvePlatformActorPackages(); + const rivetkitPackage = await resolvePlatformRivetKitPackage(); await mkdir(join(directory, "node_modules"), { recursive: true }); await symlink( - platformPackages.rivetkit, + rivetkitPackage, join(directory, "node_modules", "rivetkit"), "dir", ); @@ -415,10 +424,7 @@ export class DynamicActorRuntime { `data:text/javascript,${encodeURIComponent(ACTOR_WORKER_SOURCE)}`, ), { - workerData: { - entrypoint, - wasmPath: platformPackages.wasmPath, - }, + workerData: { entrypoint }, env: actorWorkerEnvironment(input), resourceLimits: { maxOldGenerationSizeMb: this.config.heapLimitMb, @@ -711,24 +717,15 @@ async function readBoundedBody( } } -let platformActorPackages: - | Promise<{ rivetkit: string; wasmPath: string }> - | undefined; +let platformRivetKitPackage: Promise | undefined; -function resolvePlatformActorPackages(): Promise<{ - rivetkit: string; - wasmPath: string; -}> { - platformActorPackages ??= (async () => { +function resolvePlatformRivetKitPackage(): Promise { + platformRivetKitPackage ??= (async () => { const hostRequire = createRequire(import.meta.url); const rivetkitEntry = hostRequire.resolve("rivetkit"); - const rivetkit = await findPackageRoot(rivetkitEntry); - const wasmPath = createRequire(rivetkitEntry).resolve( - "@rivetkit/rivetkit-wasm/rivetkit_wasm_bg.wasm", - ); - return { rivetkit, wasmPath }; + return findPackageRoot(rivetkitEntry); })(); - return platformActorPackages; + return platformRivetKitPackage; } async function findPackageRoot(entrypoint: string): Promise { @@ -777,7 +774,7 @@ export function actorWorkerEnvironment( endpoint.password = ""; return { NODE_ENV: "production", - RIVETKIT_RUNTIME: "wasm", + RIVETKIT_RUNTIME: "native", RIVETKIT_RUNTIME_MODE: "serverless", RIVET_ENDPOINT: endpoint.toString().replace(/\/$/u, ""), RIVET_NAMESPACE: input.namespace, @@ -958,17 +955,10 @@ class ActorAdmission { } const ACTOR_WORKER_SOURCE = ` -import { readFile } from "node:fs/promises"; import { parentPort, workerData } from "node:worker_threads"; if (!parentPort) throw new Error("Dynamic App actor worker has no parent port"); -const { registry } = await import(workerData.entrypoint); -if (typeof registry?.handler !== "function") throw new TypeError("Dynamic App actor registry is invalid"); -if (process.env.RIVETKIT_RUNTIME === "wasm" && registry.config) { - registry.config.wasm = { - ...registry.config.wasm, - initInput: await readFile(workerData.wasmPath), - }; -} +const { handler } = await import(workerData.entrypoint); +if (typeof handler !== "function") throw new TypeError("Dynamic App actor fetch handler is invalid"); const requests = new Map(); const acknowledgements = new Map(); const waitForAck = (id) => new Promise((resolve) => acknowledgements.set(id, resolve)); @@ -995,7 +985,7 @@ parentPort.on("message", (message) => { const controller = new AbortController(); requests.set(message.id, controller); try { - const response = await registry.handler(new Request(message.url, { + const response = await handler(new Request(message.url, { method: message.method, headers: message.headers, body: message.method === "GET" || message.method === "HEAD" ? undefined : message.body, diff --git a/packages/dynamic-apps/src/actors.ts b/packages/dynamic-apps/src/actors.ts index 843a4b7da..c38dd4457 100644 --- a/packages/dynamic-apps/src/actors.ts +++ b/packages/dynamic-apps/src/actors.ts @@ -805,7 +805,6 @@ async function buildRelease( entrypoint: plan.entrypoint, release, maxResponseBytes: config.maxResponseBytes, - usesRivetKit: plan.usesRivetKit, }), ), }); diff --git a/packages/dynamic-apps/src/control-plane.ts b/packages/dynamic-apps/src/control-plane.ts index b2280a447..bef8df9c0 100644 --- a/packages/dynamic-apps/src/control-plane.ts +++ b/packages/dynamic-apps/src/control-plane.ts @@ -299,19 +299,41 @@ export async function provisionAppNamespace( }; } -function serverlessAppUrl( +function serverlessAppCallback( appActorId: string, connection: ResolvedRivetConnection, -): string { +): { url: string; token?: string } { const configured = process.env.DYNAMIC_APPS_CALLBACK_URL; - if (configured) return new URL("/api/rivet", configured).toString(); - if (process.env._RIVET_COMPUTE) { - return `https://${connection.namespace}.rivet.run/api/rivet`; + if (configured) { + return { url: new URL("/api/rivet", configured).toString() }; } - return new URL( - `/gateway/${encodeURIComponent(appActorId)}/request/.agentos/apps/rivet`, - connection.endpoint, - ).toString(); + + const publicEndpoint = process.env.RIVET_PUBLIC_ENDPOINT; + const callbackConnection = publicEndpoint + ? resolveRivetConnection(publicEndpoint) + : connection; + return { + url: new URL( + `/gateway/${encodeURIComponent(appActorId)}/request/.agentos/apps/rivet`, + callbackConnection.endpoint, + ).toString(), + ...(callbackConnection.token ? { token: callbackConnection.token } : {}), + }; +} + +function resolveRivetConnection(rawEndpoint: string): ResolvedRivetConnection { + const url = new URL(rawEndpoint); + const namespace = url.username + ? decodeURIComponent(url.username) + : (process.env.RIVET_NAMESPACE ?? "default"); + const token = url.password ? decodeURIComponent(url.password) : undefined; + url.username = ""; + url.password = ""; + return { + endpoint: url.toString().replace(/\/$/, ""), + namespace, + ...(token ? { token } : {}), + }; } /** Configure the nested actor pool only after its release is ready. */ @@ -327,12 +349,14 @@ export async function configureAppNamespaceRunner( callbackConnection = resolveDefaultRivetConnection(), ): Promise { try { + const callback = serverlessAppCallback(appActorId, callbackConnection); await ensureServerlessRunnerConfig({ endpoint: runtime.endpoint, namespace: runtime.namespace, - url: serverlessAppUrl(appActorId, callbackConnection), + url: callback.url, pool: runtime.pool, token: runtime.controlToken, + callbackToken: callback.token, callbackSecret, }); } catch (error) { diff --git a/packages/dynamic-apps/src/runtime.ts b/packages/dynamic-apps/src/runtime.ts index f294fed02..e51087551 100644 --- a/packages/dynamic-apps/src/runtime.ts +++ b/packages/dynamic-apps/src/runtime.ts @@ -49,7 +49,7 @@ export function canonicalDeploymentHash(input: { deploymentIdentity?: string; }): string { const hash = createHash("sha256"); - hash.update("agentos-apps-release-v17-direct-actors\0"); + hash.update("agentos-apps-release-v19-mounted-hono-router\0"); const field = (value: string | Uint8Array) => { const bytes = typeof value === "string" ? Buffer.from(value) : value; const length = Buffer.allocUnsafe(8); @@ -78,23 +78,10 @@ export function directRunnerSource(input: { entrypoint: string; release: string; maxResponseBytes: number; - usesRivetKit?: boolean; }): string { const entrypoint = `./${normalizeAppPath(input.entrypoint)}`; - const importApplication = input.usesRivetKit - ? `const dynamicAppsModuleImportStartedAt = performance.now(); -import { Registry } from "rivetkit"; -const originalStart = Registry.prototype.start; -Registry.prototype.start = function dynamicAppsManagedStart() {}; -let application; -try { - application = await import(${JSON.stringify(entrypoint)}); -} finally { - Registry.prototype.start = originalStart; -}` - : `const dynamicAppsModuleImportStartedAt = performance.now(); -const application = await import(${JSON.stringify(entrypoint)});`; - return `${importApplication} + return `const dynamicAppsModuleImportStartedAt = performance.now(); +const application = await import(${JSON.stringify(entrypoint)}); const dynamicAppsModuleImportMs = performance.now() - dynamicAppsModuleImportStartedAt; const exported = application.default; const appFetch = typeof exported === "function" @@ -162,22 +149,19 @@ export async function dispatch(input) { `; } -/** Host-owned wrapper for the optional app-defined RivetKit registry. */ +/** Host-owned wrapper for the app's mounted actor callback handler. */ export function actorRunnerSource(entrypointInput: string): string { const entrypoint = `./${normalizeAppPath(entrypointInput)}`; - return `import { Registry } from "rivetkit"; -const originalStart = Registry.prototype.start; -Registry.prototype.start = function dynamicAppsManagedStart() {}; -let application; -try { - application = await import(${JSON.stringify(entrypoint)}); -} finally { - Registry.prototype.start = originalStart; -} -if (typeof application.registry?.handler !== "function") { - throw new TypeError("Dynamic App using RivetKit must export const registry = setup(...)"); + return `import application from ${JSON.stringify(entrypoint)}; +const appFetch = typeof application === "function" + ? application + : typeof application?.fetch === "function" + ? application.fetch.bind(application) + : undefined; +if (!appFetch) { + throw new TypeError("Dynamic App using RivetKit must default export a fetch handler with registry.handler() mounted under /api/rivet"); } -export const registry = application.registry; +export const handler = appFetch; `; } @@ -218,6 +202,7 @@ export async function ensureServerlessRunnerConfig(input: { url: string; pool: string; token?: string; + callbackToken?: string; callbackSecret: string; }): Promise { const headers = { @@ -253,7 +238,12 @@ export async function ensureServerlessRunnerConfig(input: { datacenters[datacenter.name] = { serverless: { url: input.url, - headers: { [APP_CALLBACK_SECRET_HEADER]: input.callbackSecret }, + headers: { + [APP_CALLBACK_SECRET_HEADER]: input.callbackSecret, + ...(input.callbackToken + ? { "x-rivet-token": input.callbackToken } + : {}), + }, request_lifespan: 60 * 60, metadata_poll_interval: 1_000, max_runners: 1_024, @@ -275,4 +265,21 @@ export async function ensureServerlessRunnerConfig(input: { `Rivet runner config upsert failed with HTTP ${response.status}: ${body}`, ); } + + // Publish the registry protocol synchronously so the first actor allocation + // cannot race the background metadata poller into the legacy runner path. + const refreshResponse = await controlFetch( + engineUrl( + input.endpoint, + ["runner-configs", input.pool, "refresh-metadata"], + input.namespace, + ), + { method: "POST", headers, body: "{}" }, + ); + const refreshBody = await readBoundedText(refreshResponse); + if (!refreshResponse.ok) { + throw new Error( + `Rivet runner metadata refresh failed with HTTP ${refreshResponse.status}: ${refreshBody}`, + ); + } } diff --git a/packages/dynamic-apps/tests/direct.test.ts b/packages/dynamic-apps/tests/direct.test.ts index c3f0f64e9..682cdaa75 100644 --- a/packages/dynamic-apps/tests/direct.test.ts +++ b/packages/dynamic-apps/tests/direct.test.ts @@ -247,7 +247,7 @@ describe("retained public surface", () => { pool: "app-pool", }), ).toMatchObject({ - RIVETKIT_RUNTIME: "wasm", + RIVETKIT_RUNTIME: "native", RIVET_ENDPOINT: "https://api.rivet.dev", RIVET_NAMESPACE: "app-namespace", RIVET_TOKEN: "pk_example", @@ -690,7 +690,7 @@ export async function dispatch(input) { describe("actor callback resource limits", () => { test("does not publish a worker that finishes creating during shutdown", async () => { const artifact = await makeActorArtifact(` -export const registry = { handler: () => new Response("ok") }; +export default { fetch: () => new Response("ok") }; `); const runtime = new DynamicActorRuntime(); let releaseArtifact = () => {}; @@ -740,8 +740,8 @@ export const registry = { handler: () => new Response("ok") }; test("bounds concurrent actor workers as well as idle cache entries", async () => { const artifact = await makeActorArtifact(` -export const registry = { - async handler() { +export default { + async fetch() { await new Promise((resolve) => setTimeout(resolve, 100)); return new Response("ok"); }, @@ -780,8 +780,8 @@ export const registry = { test("times out an actor handler that blocks its worker", async () => { const artifact = await makeActorArtifact(` -export const registry = { - handler() { +export default { + fetch() { while (true) {} }, }; @@ -823,8 +823,8 @@ export const registry = { test("allows an actor response stream to outlive the handler timeout", async () => { const artifact = await makeActorArtifact(` -export const registry = { - handler() { +export default { + fetch() { return new Response(new ReadableStream({ async start(controller) { controller.enqueue(new TextEncoder().encode("started-")); @@ -877,8 +877,8 @@ export const registry = { test("preserves actor worker error stacks and causes", async () => { const artifact = await makeActorArtifact(` -export const registry = { - async handler() { +export default { + async fetch() { throw new Error("outer failure", { cause: new Error("inner failure") }); }, }; @@ -905,8 +905,8 @@ export const registry = { test("admits actor callback bodies before reading them", async () => { const artifact = await makeActorArtifact(` -export const registry = { - async handler() { +export default { + async fetch() { return new Response("ok"); }, }; @@ -1027,8 +1027,8 @@ export const registry = { test("serves concurrent actor worker cache churn without losing requests", async () => { const artifact = await makeActorArtifact(` -export const registry = { - async handler() { +export default { + async fetch() { return new Response("ok"); }, }; @@ -1082,7 +1082,7 @@ export const registry = { const artifact = await makeActorArtifact(` console.log("actor stdout"); console.error("actor stderr"); -export const registry = { handler: () => new Response("ok") }; +export default { fetch: () => new Response("ok") }; `); const runtime = new DynamicActorRuntime(); try { @@ -1235,7 +1235,17 @@ async function makeActorArtifact(source: string): Promise { const directory = await mkdtemp(join(tmpdir(), "dynamic-apps-actor-test-")); const archive = join(directory, "app.tar"); await mkdir(join(directory, "actor")); - await writeFile(join(directory, "actor", "main.mjs"), source); + await writeFile(join(directory, "actor", "application.mjs"), source); + await writeFile( + join(directory, "actor", "main.mjs"), + `const { default: application } = await import("./application.mjs"); +const fetch = typeof application === "function" + ? application + : application?.fetch?.bind(application); +if (typeof fetch !== "function") throw new TypeError("invalid test fetch handler"); +export const handler = fetch; +`, + ); await writeFile( join(directory, "agentos-package.json"), JSON.stringify({ name: "dynamic-actor-test", version: "1.0.0" }), diff --git a/specs/agentos-inline-runtime-and-logging.md b/specs/agentos-inline-runtime-and-logging.md index 5f12e6a34..b4d6d733b 100644 --- a/specs/agentos-inline-runtime-and-logging.md +++ b/specs/agentos-inline-runtime-and-logging.md @@ -284,9 +284,9 @@ The direct release must be a Node-targeted ESM bundle, not a browser IIFE. - Continue rejecting native `.node` addons. - Delete the Dynamic Apps RivetKit stub entirely. - For an app importing RivetKit, build the direct bundle with the real RivetKit - WASM runtime and its WASM asset. Suppress the application's `registry.start()` - only while importing the direct entrypoint, then restore it. Actor definitions - remain real objects; only actor startup is host-managed. + runtime. The app mounts `registry.handler()` in its exported fetch router and + does not call `registry.start()` or `serve()`; actor startup remains + host-managed. - Keep the separate platform-linked actor bundle used by the existing bounded actor worker. - Continue validating both bundles before activating a release. @@ -362,8 +362,8 @@ The file must contain no import from `isolated-vm`, no custom `Request` or Node-targeted ESM dispatcher export; - reconstruct requests with Node's real Web APIs; - use `Buffer` for bounded Base64 conversion; -- temporarily suppress `Registry.prototype.start` while importing an - actor-enabled app for direct serving; +- import the actor-enabled app's mounted fetch router without patching RivetKit + lifecycle methods; - preserve response status, status text, ordered headers, repeated `set-cookie`, body limits, and phase timing; and - leave `actorRunnerSource()` and authenticated callback configuration intact. diff --git a/tests/e2e/dynamic-apps/src/run.ts b/tests/e2e/dynamic-apps/src/run.ts index f0e2726b4..1505371b3 100644 --- a/tests/e2e/dynamic-apps/src/run.ts +++ b/tests/e2e/dynamic-apps/src/run.ts @@ -7,6 +7,7 @@ import getPort from "get-port"; const fast = process.argv.includes("--fast"); const buildOnly = process.argv.includes("--build-only"); +const sanity = process.argv.includes("--sanity"); const root = await mkdtemp(join(tmpdir(), "agentos-apps-e2e-")); const databasePath = join(root, "db"); await mkdir(databasePath, { recursive: true }); @@ -59,7 +60,7 @@ try { } delete process.env.RIVET_ENGINE; delete process.env.RIVET_RUN_ENGINE; - await import("./verify.js"); + await import(sanity ? "./sanity.js" : "./verify.js"); } finally { await stopEngine(engine); await rm(root, { recursive: true, force: true }); diff --git a/tests/e2e/dynamic-apps/src/sanity.ts b/tests/e2e/dynamic-apps/src/sanity.ts new file mode 100644 index 000000000..fcba20eb8 --- /dev/null +++ b/tests/e2e/dynamic-apps/src/sanity.ts @@ -0,0 +1,71 @@ +import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps"; +import { createClient } from "rivetkit/client"; + +const appId = `dynamic-apps-sanity-${Date.now()}`; +const deployment = await deployApp({ + appId, + files: { + "package.json": JSON.stringify({ + name: "dynamic-apps-sanity", + version: "1.0.0", + private: true, + type: "module", + main: "index.js", + dependencies: { hono: "4.13.3", rivetkit: "2.3.11" }, + }), + "index.js": ` +import { Hono } from "hono"; +import { actor, setup } from "rivetkit"; +import { db } from "rivetkit/db"; +const counter = actor({ + db: db({ async onMigrate(database) { + await database.execute("CREATE TABLE IF NOT EXISTS counts (id TEXT PRIMARY KEY, value INTEGER NOT NULL)"); + }}), + actions: { + async increment(c) { + await c.db.execute("INSERT INTO counts (id, value) VALUES ('main', 1) ON CONFLICT(id) DO UPDATE SET value = value + 1"); + const rows = await c.db.execute("SELECT value FROM counts WHERE id = 'main'"); + return Number(rows[0]?.value ?? 0); + }, + }, +}); +const registry = setup({ use: { counter } }); +const app = new Hono(); +app.all("/api/rivet/*", (c) => registry.handler(c.req.raw)); +app.all("*", () => Response.json({ ok: true, path: "direct" })); +export default app; +`, + }, +}); + +const direct = await appsRouter.request(`/${appId}/`); +if (!direct.ok || !((await direct.json()) as { ok?: boolean }).ok) { + throw new Error(`direct sanity failed with HTTP ${direct.status}`); +} + +const client = createClient({ + namespace: deployment.namespace, + poolName: deployment.pool, +}) as unknown as { + counter: { getOrCreate(key: string[]): { increment(): Promise } }; + dispose(): Promise; +}; +const counter = client.counter.getOrCreate(["sanity"]); +const first = await counter.increment(); +const second = await counter.increment(); +await client.dispose(); +if (first !== 1 || second !== 2) + throw new Error(`SQLite sanity returned ${first}, ${second}`); + +console.log( + JSON.stringify( + { + event: "dynamic_apps_sanity_passed", + deployment, + direct: true, + sqlite: [first, second], + }, + null, + 2, + ), +); diff --git a/tests/e2e/dynamic-apps/src/verify.ts b/tests/e2e/dynamic-apps/src/verify.ts index 3a3928beb..a38a229cc 100644 --- a/tests/e2e/dynamic-apps/src/verify.ts +++ b/tests/e2e/dynamic-apps/src/verify.ts @@ -149,10 +149,12 @@ async function deployActorFixture() { type: "module", main: "index.js", dependencies: { + hono: "4.13.3", rivetkit: "2.3.11", }, }), "index.js": ` +import { Hono } from "hono"; import { actor, event, setup } from "rivetkit"; const counter = actor({ @@ -170,12 +172,11 @@ const counter = actor({ }, }); -export const registry = setup({ use: { counter } }); -registry.start(); - -export default function fetch() { - return Response.json({ ok: true, workload: "actor-and-direct-http" }); -} +const registry = setup({ use: { counter } }); +const app = new Hono(); +app.all("/api/rivet/*", (c) => registry.handler(c.req.raw)); +app.all("*", () => Response.json({ ok: true, workload: "actor-and-direct-http" })); +export default app; `, }, scaling: {